Compare commits
3 Commits
fd8df2abd5
...
58da08e26f
| Author | SHA1 | Date | |
|---|---|---|---|
| 58da08e26f | |||
| 35bd0a7278 | |||
| 0ff77954db |
@@ -16,6 +16,7 @@ enable_debug_commands: true
|
||||
prompt_char_limit: 300000
|
||||
enable_chunking: false
|
||||
enable_step_by_step: true
|
||||
auto_lint: true
|
||||
|
||||
# New tool auto-accept config
|
||||
tool_auto_accept:
|
||||
|
||||
15
README.md
15
README.md
@@ -83,3 +83,18 @@ commands:
|
||||
The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
|
||||
|
||||
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!
|
||||
|
||||
## Test when developing
|
||||
add new plugin to runtimepath
|
||||
```vim
|
||||
:set rtp^=~/temp_plugins/chatgpt.vim
|
||||
```
|
||||
Clear Lua module cache
|
||||
```vim
|
||||
:lua for k in pairs(package.loaded) do if k:match("^chatgpt_nvim") then package.loaded[k]=nil end end
|
||||
```
|
||||
Load new plugin code
|
||||
```vim
|
||||
:runtime plugin/chatgpt.vim
|
||||
```
|
||||
|
||||
|
||||
@@ -50,12 +50,14 @@ function M.load()
|
||||
improved_debug = false,
|
||||
enable_chunking = false,
|
||||
enable_step_by_step = true,
|
||||
enable_debug_commands = false,
|
||||
|
||||
-- New table for tool auto-accept configuration
|
||||
-- If auto_lint is true, we only do LSP-based checks.
|
||||
auto_lint = false,
|
||||
|
||||
tool_auto_accept = {
|
||||
readFile = false,
|
||||
editFile = false,
|
||||
replace_in_file = false,
|
||||
executeCommand = false,
|
||||
}
|
||||
}
|
||||
@@ -106,11 +108,12 @@ function M.load()
|
||||
if type(result.enable_step_by_step) == "boolean" then
|
||||
config.enable_step_by_step = result.enable_step_by_step
|
||||
end
|
||||
if type(result.enable_debug_commands) == "boolean" then
|
||||
config.enable_debug_commands = result.enable_debug_commands
|
||||
|
||||
-- auto_lint controls whether we do LSP-based checks
|
||||
if type(result.auto_lint) == "boolean" then
|
||||
config.auto_lint = result.auto_lint
|
||||
end
|
||||
|
||||
-- Load tool_auto_accept if present
|
||||
if type(result.tool_auto_accept) == "table" then
|
||||
for k, v in pairs(result.tool_auto_accept) do
|
||||
if config.tool_auto_accept[k] ~= nil and type(v) == "boolean" then
|
||||
@@ -124,7 +127,7 @@ function M.load()
|
||||
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
|
||||
end
|
||||
|
||||
-- Merge the default prompt blocks with the config's initial prompt
|
||||
-- Merge default prompt blocks
|
||||
if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
|
||||
local merged_prompt = {}
|
||||
for _, block_name in ipairs(config.default_prompt_blocks) do
|
||||
|
||||
@@ -67,7 +67,6 @@ end
|
||||
-- PROMPT CONSTRUCTION
|
||||
------------------------------------------------------------------------------
|
||||
local function build_tools_available_block()
|
||||
-- We'll list each tool from tools_module.available_tools
|
||||
local lines = {}
|
||||
lines[#lines+1] = "<tools_available>"
|
||||
for _, t in ipairs(tools_module.available_tools) do
|
||||
@@ -94,7 +93,7 @@ local function build_prompt(user_input, dirs, conf)
|
||||
|
||||
-- 3) <task>
|
||||
local task_lines = {}
|
||||
table.insert(task_lines, "<task>")
|
||||
task_lines[#task_lines+1] = "<task>"
|
||||
task_lines[#task_lines+1] = user_input
|
||||
for _, file_path in ipairs(initial_files) do
|
||||
task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path)
|
||||
@@ -154,7 +153,7 @@ local function handle_step_by_step_if_needed(prompt, conf)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- :ChatGPT Command
|
||||
-- :ChatGPT
|
||||
------------------------------------------------------------------------------
|
||||
local function run_chatgpt_command()
|
||||
local conf = config.load()
|
||||
@@ -221,7 +220,7 @@ local function run_chatgpt_command()
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- :ChatGPTPaste Command
|
||||
-- :ChatGPTPaste
|
||||
------------------------------------------------------------------------------
|
||||
local function run_chatgpt_paste_command()
|
||||
local conf = config.load()
|
||||
@@ -239,29 +238,21 @@ local function run_chatgpt_paste_command()
|
||||
return
|
||||
end
|
||||
|
||||
-- 1) Debug commands
|
||||
if data.commands and conf.enable_debug_commands then
|
||||
local results = {}
|
||||
for _, cmd in ipairs(data.commands) do
|
||||
table.insert(results, require('chatgpt_nvim').execute_debug_command(cmd, conf))
|
||||
end
|
||||
local output = table.concat(results, "\n\n")
|
||||
copy_to_clipboard(output)
|
||||
print("Debug command results copied to clipboard!")
|
||||
return
|
||||
end
|
||||
|
||||
-- 2) Tools (handle multiple calls in one request)
|
||||
-- Check if we have tools
|
||||
if data.tools then
|
||||
-- Must also verify project name
|
||||
if not data.project_name or data.project_name ~= conf.project_name then
|
||||
vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
|
||||
return
|
||||
end
|
||||
|
||||
local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file)
|
||||
-- If the output is too large (over conf.prompt_char_limit?), we might respond with a special message.
|
||||
copy_to_clipboard(output_messages)
|
||||
print("Tool call results have been processed and copied to clipboard.")
|
||||
return
|
||||
end
|
||||
|
||||
-- 3) If we see project_name & files => final changes or file requests
|
||||
-- (If the user is still using older YAML style, we handle it, but not recommended.)
|
||||
-- If we see project_name & files => older YAML style. We handle it but it's discouraged now.
|
||||
if data.project_name and data.files then
|
||||
if data.project_name ~= conf.project_name then
|
||||
vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
|
||||
@@ -294,7 +285,6 @@ local function run_chatgpt_paste_command()
|
||||
vim.api.nvim_err_writeln("Preview not closed in time. Aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local final_files = data.files
|
||||
@@ -354,7 +344,7 @@ local function run_chatgpt_paste_command()
|
||||
"\n\nProject name: " .. (conf.project_name or ""),
|
||||
"\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n",
|
||||
table.concat(file_sections, "\n"),
|
||||
"\n\nIf you need more files, please respond again in YAML listing additional files. Or use the new 'tools' array approach to read/edit files. If you have all info, provide final changes or just proceed."
|
||||
"\n\nIf you need more files, please respond again in YAML listing additional files, or use the 'tools:' approach. If you have all info, provide final changes or continue your instructions."
|
||||
}
|
||||
|
||||
local prompt = table.concat(sections, "\n")
|
||||
@@ -375,7 +365,7 @@ local function run_chatgpt_paste_command()
|
||||
print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.")
|
||||
end
|
||||
else
|
||||
vim.api.nvim_err_writeln("Invalid response. Expected either 'tools' or 'project_name & files' in YAML.")
|
||||
vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -425,94 +415,4 @@ M.run_chatgpt_command = run_chatgpt_command
|
||||
M.run_chatgpt_paste_command = run_chatgpt_paste_command
|
||||
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
|
||||
|
||||
-- Provide debug commands for "ls" and "grep"
|
||||
M.execute_debug_command = function(cmd, conf)
|
||||
if type(cmd) ~= "table" or not cmd.command then
|
||||
return "Invalid command object."
|
||||
end
|
||||
local command = cmd.command
|
||||
if command == "ls" then
|
||||
local dir = cmd.dir or "."
|
||||
local args = cmd.args or {}
|
||||
local cmd_str = "ls"
|
||||
if #args > 0 then
|
||||
cmd_str = cmd_str .. " " .. table.concat(args, " ")
|
||||
end
|
||||
cmd_str = cmd_str .. " " .. dir
|
||||
|
||||
local handle = io.popen(cmd_str)
|
||||
if not handle then
|
||||
return "Failed to run ls command."
|
||||
end
|
||||
local result = handle:read("*a") or ""
|
||||
handle:close()
|
||||
return "Listing files in: " .. dir .. "\n" .. result
|
||||
|
||||
elseif command == "grep" then
|
||||
local args = cmd.args or {}
|
||||
if #args == 0 then
|
||||
local pattern = cmd.pattern
|
||||
local target = cmd.target
|
||||
if not pattern or not target then
|
||||
return "Usage for grep: {command='grep', args=['-r','pattern','target']} or {pattern='<text>', target='<path>'}"
|
||||
end
|
||||
local stat = vim.loop.fs_stat(target)
|
||||
if not stat then
|
||||
return "Cannot grep: target path does not exist"
|
||||
end
|
||||
|
||||
local function do_grep(search_string, filepath)
|
||||
local c = read_file(filepath)
|
||||
if not c then
|
||||
return "Could not read file: " .. filepath
|
||||
end
|
||||
local lines = {}
|
||||
local line_num = 0
|
||||
for line in c:gmatch("([^\n]*)\n?") do
|
||||
line_num = line_num + 1
|
||||
if line:find(search_string, 1, true) then
|
||||
lines[#lines+1] = filepath .. ":" .. line_num .. ":" .. line
|
||||
end
|
||||
end
|
||||
return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
if stat.type == "directory" then
|
||||
local h = io.popen("ls -p " .. target .. " | grep -v /")
|
||||
if not h then
|
||||
return "Failed to read directory contents for grep."
|
||||
end
|
||||
local all_files = {}
|
||||
for file in h:read("*a"):gmatch("[^\n]+") do
|
||||
all_files[#all_files+1] = target .. "/" .. file
|
||||
end
|
||||
h:close()
|
||||
local results = {}
|
||||
for _, f in ipairs(all_files) do
|
||||
local fstat = vim.loop.fs_stat(f)
|
||||
if fstat and fstat.type == "file" then
|
||||
results[#results+1] = do_grep(pattern, f)
|
||||
end
|
||||
end
|
||||
return table.concat(results, "\n")
|
||||
else
|
||||
return do_grep(pattern, target)
|
||||
end
|
||||
|
||||
else
|
||||
-- new approach with flags/args
|
||||
local cmd_str = "grep " .. table.concat(args, " ")
|
||||
local handle = io.popen(cmd_str)
|
||||
if not handle then
|
||||
return "Failed to run grep command."
|
||||
end
|
||||
local result = handle:read("*a") or ""
|
||||
handle:close()
|
||||
return result
|
||||
end
|
||||
else
|
||||
return "Unknown command: " .. command
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -44,7 +44,7 @@ local M = {
|
||||
5. **Styling & CSS Management**
|
||||
- Use your preferred styling approach (CSS Modules, [Tailwind CSS](https://tailwindcss.com/), or standard CSS/SCSS files).
|
||||
- Keep global styles minimal, focusing on utility classes or base styling; keep component-level styles scoped whenever possible.
|
||||
- If using CSS-in-JS solutions or third-party libraries, ensure they integrate cleanly with Solid’s reactivity.
|
||||
- If using CSS-in-JS solutions or third-party libraries, ensure they integrate cleanly with Solid’s reactivity.
|
||||
|
||||
6. **TypeScript & Linting**
|
||||
- Use **TypeScript** to ensure type safety and improve maintainability.
|
||||
@@ -81,7 +81,7 @@ local M = {
|
||||
1. **Go Modules**
|
||||
- Use a single `go.mod` file at the project root for module management.
|
||||
- Ensure you use proper import paths based on the module name.
|
||||
- If you refer to internal packages, use relative paths consistent with the module’s structure (e.g., `moduleName/internal/packageA`).
|
||||
- If you refer to internal packages, use relative paths consistent with the module’s structure (e.g., `moduleName/internal/packageA`).
|
||||
|
||||
2. **Package Structure**
|
||||
- Each folder should contain exactly one package.
|
||||
@@ -110,21 +110,21 @@ local M = {
|
||||
4. **Coding Best Practices**
|
||||
- Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols).
|
||||
- Keep functions short, focused, and tested.
|
||||
- Use Go’s standard library where possible before adding third-party dependencies.
|
||||
- Use Go’s standard library where possible before adding third-party dependencies.
|
||||
- When introducing new functions or types, ensure they are uniquely named to avoid collisions.
|
||||
|
||||
5. **Import Management**
|
||||
- Ensure that every import is actually used in your code.
|
||||
- Remove unused imports to keep your code clean and maintainable.
|
||||
- Include all necessary imports for anything referenced in your code to avoid missing imports.
|
||||
- Verify that any introduced import paths match your module’s structure and do not cause naming conflicts.
|
||||
- Verify that any introduced import paths match your module’s structure and do not cause naming conflicts.
|
||||
|
||||
6. **Output Format**
|
||||
- Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule.
|
||||
- When explaining your reasoning, include any relevant architectural trade-offs and rationale (e.g., “I placed function X in package Y to keep the domain-specific logic separate from the main execution flow.”).
|
||||
- If you modify an existing file, specify precisely which changes or additions you are making.
|
||||
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with Golang’s best practices for large, modular projects.
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with Golang’s best practices for large, modular projects.
|
||||
]],
|
||||
["typo3-development"] = [[
|
||||
### TYPO3 Development Guidelines
|
||||
@@ -150,10 +150,10 @@ local M = {
|
||||
- Keep site configuration in `config/sites/` (for TYPO3 v9+).
|
||||
|
||||
2. **Extension Development**
|
||||
- Create custom functionality as separate extensions (site packages, domain-specific extensions, etc.) following TYPO3’s recommended structure:
|
||||
- Create custom functionality as separate extensions (site packages, domain-specific extensions, etc.) following TYPO3’s recommended structure:
|
||||
- **Key files**: `ext_emconf.php`, `ext_localconf.php`, `ext_tables.php`, `ext_tables.sql`, `Configuration/`, `Classes/`, `Resources/`.
|
||||
- Use **PSR-4** autoloading and name extensions logically (e.g., `my_sitepackage`, `my_blogextension`).
|
||||
- Keep your extension’s code under `Classes/` (e.g., Controllers, Models, Services).
|
||||
- Keep your extension’s code under `Classes/` (e.g., Controllers, Models, Services).
|
||||
- Place Fluid templates, partials, and layouts under `Resources/Private/` (e.g., `Resources/Private/Templates`, `Resources/Private/Partials`, `Resources/Private/Layouts`).
|
||||
|
||||
3. **Configuration (TypoScript & TCA)**
|
||||
@@ -186,10 +186,10 @@ local M = {
|
||||
7. **Output Format**
|
||||
- Present any generated source code or configuration files in a well-organized structure.
|
||||
- Clearly indicate where each file should be placed in the TYPO3 directory layout.
|
||||
- When explaining your reasoning, include any relevant architectural decisions (e.g., “I created a separate extension for blog functionality to keep it isolated from the site’s main configuration.”).
|
||||
- When explaining your reasoning, include any relevant architectural decisions (e.g., “I created a separate extension for blog functionality to keep it isolated from the site’s main configuration.”).
|
||||
- If you modify or extend an existing file, specify precisely which changes or additions you are making.
|
||||
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3’s best practices for large, maintainable projects.
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3’s best practices for large, maintainable projects.
|
||||
]],
|
||||
["rust-development"] = [[
|
||||
### Rust Development Guidelines
|
||||
@@ -204,11 +204,11 @@ local M = {
|
||||
2. **Crates & Packages**
|
||||
- Split the application into logical crates (libraries and/or binaries).
|
||||
- Each crate should have a single main **library** (`lib.rs`) or **binary** (`main.rs`) in its `src/` folder.
|
||||
- Name crates, modules, and files clearly, following Rust’s naming conventions (e.g., `snake_case` for files/modules, `PascalCase` for types).
|
||||
- Name crates, modules, and files clearly, following Rust’s naming conventions (e.g., `snake_case` for files/modules, `PascalCase` for types).
|
||||
- Avoid duplicating the same function or type in multiple crates; share common functionality via a dedicated library crate if needed.
|
||||
|
||||
3. **Folder & Module Structure**
|
||||
- Organize code within each crate using Rust’s module system, keeping related functions and types in logical modules/submodules.
|
||||
- Organize code within each crate using Rust’s module system, keeping related functions and types in logical modules/submodules.
|
||||
- A typical directory layout for a workspace with multiple crates might look like:
|
||||
```
|
||||
myproject/
|
||||
@@ -226,7 +226,7 @@ local M = {
|
||||
├── target/
|
||||
└── ...
|
||||
```
|
||||
- If you have integration tests, store them in a `tests/` folder at the crate root, or use the workspace root’s `tests/` directory if they span multiple crates.
|
||||
- If you have integration tests, store them in a `tests/` folder at the crate root, or use the workspace root’s `tests/` directory if they span multiple crates.
|
||||
|
||||
4. **Coding & Documentation Best Practices**
|
||||
- Write **idiomatic Rust** code:
|
||||
@@ -256,54 +256,40 @@ local M = {
|
||||
["basic-prompt"] = [[
|
||||
### Basic Prompt
|
||||
|
||||
1. **Analyze Required Files**
|
||||
- Check the project structure (or any provided file list).
|
||||
- If you identify a file reference or function you do not have, **produce a YAML snippet** requesting that file’s content in full. For example:
|
||||
```yaml
|
||||
project_name: my_project
|
||||
files:
|
||||
- path: "relative/path/to/needed_file"
|
||||
```
|
||||
- Do not proceed with final changes if you lack the necessary file contents.
|
||||
- Always provide the entire file content when you do include a file.
|
||||
You are assisting me in a coding workflow for a project (e.g., "my_project"). Whenever you need to inspect or modify files, or execute commands, you must provide both:
|
||||
|
||||
2. **Request File Contents**
|
||||
- For every file you need but don’t have, provide a short YAML snippet (like above) to retrieve it.
|
||||
- Ask any clarifying questions outside the YAML code block.
|
||||
1. `project_name: "<actual_project_name>"` (matching the real project name)
|
||||
2. A `tools:` array describing the operations you want to perform.
|
||||
|
||||
3. **Provide Output YAML**
|
||||
- When you have all information, output the final YAML in this format:
|
||||
```yaml
|
||||
project_name: my_project
|
||||
**Example** (substitute `<actual_project_name>` with the real name):
|
||||
```yaml
|
||||
project_name: "my_project"
|
||||
tools:
|
||||
- tool: "readFile"
|
||||
path: "relative/path/to/file"
|
||||
|
||||
files:
|
||||
- path: "src/main.py"
|
||||
content: |
|
||||
# Updated main function
|
||||
def main():
|
||||
print("Hello, World!")
|
||||
- tool: "replace_in_file"
|
||||
path: "relative/path/to/file"
|
||||
replacements:
|
||||
- search: "old text"
|
||||
replace: "new text"
|
||||
|
||||
- path: "docs/README.md"
|
||||
delete: true
|
||||
```
|
||||
- `project_name` must match the project’s configured name.
|
||||
- Each file item in `files` must have `path` and either `content` or `delete: true`.
|
||||
- tool: "editFile"
|
||||
path: "relative/path/to/file"
|
||||
content: |
|
||||
# Full updated file content here
|
||||
|
||||
4. **Explain Changes**
|
||||
- After the final YAML, add a brief explanation of the modifications (outside the YAML).
|
||||
- tool: "executeCommand"
|
||||
command: "ls -la"
|
||||
```
|
||||
|
||||
5. **Iterate as Needed**
|
||||
- If further context or changes are required, repeat steps to request files or clarifications.
|
||||
|
||||
6. **Important Notes**
|
||||
- Never modify or delete a file you haven’t explicitly requested or received.
|
||||
- Use comments in code only to clarify code logic, not to explain your thought process.
|
||||
- Explanations go outside the YAML.
|
||||
|
||||
7. **Best Practices**
|
||||
- Keep file paths accurate to avoid mistakes during implementation.
|
||||
- Maintain a clear, logical structure for your changes.
|
||||
- Use the final YAML consistently for clarity.
|
||||
**Key Points**:
|
||||
- Always include `project_name: "<actual_project_name>"` in the same YAML as `tools`.
|
||||
- If you only need one tool, include just one object in the `tools` array.
|
||||
- If multiple tools are needed, list them sequentially under `tools`.
|
||||
- Allways run at least one tool (e.g., `readFile`, `editFile`, `executeCommand`), exept you have finished.
|
||||
- The plugin will verify the `project_name` is correct before running any tools.
|
||||
- If the response grows too large, I'll guide you to break it into smaller steps.
|
||||
]],
|
||||
["secure-coding"] = [[
|
||||
### Secure Coding Guidelines
|
||||
@@ -370,33 +356,10 @@ local M = {
|
||||
["step-prompt"] = [[
|
||||
It appears this request might exceed the model's prompt character limit if done all at once.
|
||||
Please break down the tasks into smaller steps and handle them one by one.
|
||||
At each step, we'll provide relevant files or context if needed.
|
||||
Thank you!
|
||||
]],
|
||||
["file-selection-instructions"] = [[
|
||||
Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd)
|
||||
]],
|
||||
["debug-commands-info"] = [[
|
||||
### Debugging Commands
|
||||
|
||||
If you need debugging commands, include them in your YAML response as follows:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- command: "ls"
|
||||
args: ["-l", "path/to/directory"]
|
||||
|
||||
- command: "grep"
|
||||
args: ["-r", "searchString", "path/to/file/or/directory"]
|
||||
```
|
||||
|
||||
The "ls" command uses the system's 'ls' command to list directory contents. You can pass flags or additional arguments in `args`.
|
||||
The "grep" command searches for a given pattern in files or directories, again receiving flags or additional arguments in `args`.
|
||||
If you omit `args` for grep, you can still use the older format with `pattern` and `target` for backward compatibility.
|
||||
|
||||
This are the only two commands supported at the moment.
|
||||
|
||||
When these commands are present and `enable_debug_commands` is true, I'll execute them and return the results in the clipboard.
|
||||
]]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
local handler = require("chatgpt_nvim.handler")
|
||||
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
||||
@@ -14,13 +16,21 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file
|
||||
return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path)
|
||||
end
|
||||
|
||||
-- 1) Write the new content
|
||||
handler.write_file(path, new_content, conf)
|
||||
|
||||
local msg = {}
|
||||
msg[#msg+1] = string.format("Tool [edit_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path)
|
||||
msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n"
|
||||
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content)
|
||||
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
|
||||
|
||||
-- 2) If auto_lint => run robust LSP approach
|
||||
if conf.auto_lint then
|
||||
local diag_str = robust_lsp.lsp_check_file_content(path, new_content, 3000) -- 3s wait
|
||||
msg[#msg+1] = "\n" .. diag_str
|
||||
end
|
||||
|
||||
return table.concat(msg, "")
|
||||
end
|
||||
|
||||
|
||||
167
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
167
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
@@ -0,0 +1,167 @@
|
||||
local api = vim.api
|
||||
local lsp = vim.lsp
|
||||
|
||||
local M = {}
|
||||
|
||||
local function guess_filetype(path)
|
||||
local ext = path:match("^.+%.([^./\\]+)$")
|
||||
if not ext then return nil end
|
||||
ext = ext:lower()
|
||||
local map = {
|
||||
lua = "lua",
|
||||
go = "go",
|
||||
rs = "rust",
|
||||
js = "javascript",
|
||||
jsx = "javascriptreact",
|
||||
ts = "typescript",
|
||||
tsx = "typescriptreact",
|
||||
php = "php",
|
||||
}
|
||||
return map[ext]
|
||||
end
|
||||
|
||||
local function create_scratch_buffer(path, content)
|
||||
local bufnr = api.nvim_create_buf(false, true)
|
||||
if bufnr == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
api.nvim_buf_set_name(bufnr, path)
|
||||
|
||||
local lines = {}
|
||||
for line in (content.."\n"):gmatch("(.-)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
|
||||
api.nvim_buf_set_option(bufnr, "swapfile", false)
|
||||
return bufnr
|
||||
end
|
||||
|
||||
local function attach_existing_lsp_client(bufnr, filetype)
|
||||
local clients = lsp.get_active_clients()
|
||||
if not clients or #clients == 0 then
|
||||
return nil, "No active LSP clients"
|
||||
end
|
||||
|
||||
for _, client in ipairs(clients) do
|
||||
local ft_conf = client.config.filetypes or {}
|
||||
if vim.tbl_contains(ft_conf, filetype) then
|
||||
if not client.attached_buffers[bufnr] then
|
||||
vim.lsp.buf_attach_client(bufnr, client.id)
|
||||
end
|
||||
return client.id
|
||||
end
|
||||
end
|
||||
return nil, ("No active LSP client supports filetype '%s'"):format(filetype)
|
||||
end
|
||||
|
||||
local function send_did_open(bufnr, client_id, path, filetype)
|
||||
local client = lsp.get_client_by_id(client_id)
|
||||
if not client then
|
||||
return "Invalid client ID."
|
||||
end
|
||||
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
|
||||
local uri = vim.uri_from_fname(path)
|
||||
|
||||
local didOpenParams = {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
languageId = filetype,
|
||||
version = 0,
|
||||
text = text,
|
||||
}
|
||||
}
|
||||
|
||||
client.rpc.notify("textDocument/didOpen", didOpenParams)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function send_did_change(bufnr, client_id)
|
||||
local client = lsp.get_client_by_id(client_id)
|
||||
if not client then return end
|
||||
|
||||
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
local version = 1
|
||||
|
||||
client.rpc.notify("textDocument/didChange", {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
version = version,
|
||||
},
|
||||
contentChanges = {
|
||||
{ text = text }
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
local function wait_for_diagnostics(bufnr, timeout_ms)
|
||||
local done = false
|
||||
local result_diags = {}
|
||||
|
||||
local augrp = api.nvim_create_augroup("chatgpt_lsp_diag_"..bufnr, { clear = true })
|
||||
api.nvim_create_autocmd("DiagnosticChanged", {
|
||||
group = augrp,
|
||||
callback = function(args)
|
||||
if args.buf == bufnr then
|
||||
local diags = vim.diagnostic.get(bufnr)
|
||||
result_diags = diags
|
||||
done = true
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local waited = 0
|
||||
local interval = 50
|
||||
while not done and waited < timeout_ms do
|
||||
vim.cmd(("sleep %d m"):format(interval))
|
||||
waited = waited + interval
|
||||
end
|
||||
|
||||
pcall(api.nvim_del_augroup_by_id, augrp)
|
||||
return result_diags
|
||||
end
|
||||
|
||||
local function diagnostics_to_string(diags)
|
||||
if #diags == 0 then
|
||||
return "No LSP diagnostics reported."
|
||||
end
|
||||
local lines = { "--- LSP Diagnostics ---" }
|
||||
for _, d in ipairs(diags) do
|
||||
local sev = vim.diagnostic.severity[d.severity] or d.severity
|
||||
lines[#lines+1] = string.format("Line %d [%s]: %s", d.lnum + 1, sev, d.message)
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function M.lsp_check_file_content(path, new_content, timeout_ms)
|
||||
local filetype = guess_filetype(path) or "plaintext"
|
||||
|
||||
local bufnr = create_scratch_buffer(path, new_content)
|
||||
if not bufnr then
|
||||
return "(LSP) Could not create scratch buffer."
|
||||
end
|
||||
|
||||
local client_id, err = attach_existing_lsp_client(bufnr, filetype)
|
||||
if not client_id then
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
return "(LSP) " .. (err or "No suitable LSP client.")
|
||||
end
|
||||
|
||||
local err2 = send_did_open(bufnr, client_id, path, filetype)
|
||||
if err2 then
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
return "(LSP) " .. err2
|
||||
end
|
||||
|
||||
send_did_change(bufnr, client_id)
|
||||
|
||||
local diags = wait_for_diagnostics(bufnr, timeout_ms or 2000)
|
||||
local diag_str = diagnostics_to_string(diags)
|
||||
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
return diag_str
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,8 +1,7 @@
|
||||
local tools_module = require("chatgpt_nvim.tools")
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Simple destructive command check
|
||||
local function is_destructive_command(cmd)
|
||||
if not cmd then return false end
|
||||
local destructive_list = { "rm", "sudo", "mv", "cp" }
|
||||
@@ -14,51 +13,66 @@ local function is_destructive_command(cmd)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Prompt user if not auto-accepted or if command is destructive
|
||||
local function prompt_user_tool_accept(tool_call, conf)
|
||||
local function ask_user(msg)
|
||||
vim.api.nvim_out_write(msg .. " [y/N]: ")
|
||||
local ans = vim.fn.input("")
|
||||
if ans:lower() == "y" then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local auto_accept = conf.tool_auto_accept[tool_call.tool]
|
||||
|
||||
-- If this is an executeCommand and we see it's destructive, force a user prompt
|
||||
if tool_call.tool == "executeCommand" and auto_accept then
|
||||
if is_destructive_command(tool_call.command) then
|
||||
auto_accept = false
|
||||
end
|
||||
end
|
||||
|
||||
if not auto_accept then
|
||||
return ask_user(("Tool request: %s -> Accept?"):format(tool_call.tool or "unknown"))
|
||||
else
|
||||
if auto_accept then
|
||||
-- If auto-accepted and not destructive, no prompt needed
|
||||
return true
|
||||
else
|
||||
-- Build some context about the tool request
|
||||
local msg = ("Tool request: %s\n"):format(tool_call.tool or "unknown")
|
||||
if tool_call.path then
|
||||
msg = msg .. ("Path: %s\n"):format(tool_call.path)
|
||||
end
|
||||
if tool_call.command then
|
||||
msg = msg .. ("Command: %s\n"):format(tool_call.command)
|
||||
end
|
||||
if tool_call.replacements then
|
||||
msg = msg .. ("Replacements: %s\n"):format(vim.inspect(tool_call.replacements))
|
||||
end
|
||||
|
||||
msg = msg .. "Accept this tool request? [y/N]: "
|
||||
|
||||
-- Force a screen redraw so the user sees the prompt properly
|
||||
vim.cmd("redraw")
|
||||
|
||||
local ans = vim.fn.input(msg)
|
||||
return ans:lower() == "y"
|
||||
end
|
||||
end
|
||||
|
||||
-- We'll pass references to `read_file` from init.
|
||||
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
|
||||
local messages = {}
|
||||
|
||||
for _, call in ipairs(tools) do
|
||||
-- Prompt user acceptance
|
||||
local accepted = prompt_user_tool_accept(call, conf)
|
||||
if not accepted then
|
||||
table.insert(messages, string.format("Tool [%s] was rejected by user.", call.tool or "nil"))
|
||||
table.insert(messages, ("Tool [%s] was rejected by user."):format(call.tool or "nil"))
|
||||
else
|
||||
local tool_impl = tools_module.tools_by_name[call.tool]
|
||||
if tool_impl then
|
||||
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
|
||||
table.insert(messages, msg)
|
||||
else
|
||||
table.insert(messages, string.format("Unknown tool type: '%s'", call.tool or "nil"))
|
||||
table.insert(messages, ("Unknown tool type: '%s'"):format(call.tool or "nil"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(messages, "\n\n")
|
||||
local combined = table.concat(messages, "\n\n")
|
||||
local limit = conf.prompt_char_limit or 8000
|
||||
if #combined > limit then
|
||||
return ("The combined tool output is too large (%d chars). Please break down the operations into smaller steps."):format(#combined)
|
||||
end
|
||||
return combined
|
||||
end
|
||||
|
||||
M.handle_tool_calls = handle_tool_calls
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
local handler = require("chatgpt_nvim.handler")
|
||||
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function search_and_replace(original, replacements)
|
||||
-- Basic approach: do a global plain text replace for each entry
|
||||
local updated = original
|
||||
for _, r in ipairs(replacements) do
|
||||
local search_str = r.search or ""
|
||||
local replace_str = r.replace or ""
|
||||
-- Here we do a global plain text replacement
|
||||
updated = updated:gsub(search_str, replace_str)
|
||||
end
|
||||
return updated
|
||||
@@ -41,6 +40,11 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file
|
||||
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data)
|
||||
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
|
||||
|
||||
if conf.auto_lint then
|
||||
local diag_str = robust_lsp.lsp_check_file_content(path, updated_data, 3000)
|
||||
msg[#msg+1] = "\n" .. diag_str
|
||||
end
|
||||
|
||||
return table.concat(msg, "")
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user