From 35bd0a72781fe7c1536012176cd6062c22916256 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Fri, 31 Jan 2025 10:43:28 +0100 Subject: [PATCH] feat: add lint tool --- README.md | 15 ++++++++ lua/chatgpt_nvim/config.lua | 10 ++++- lua/chatgpt_nvim/tools/edit_file.lua | 15 ++++++++ lua/chatgpt_nvim/tools/lint.lua | 42 +++++++++++++++++++++ lua/chatgpt_nvim/tools/manager.lua | 43 ++++++++++++++-------- lua/chatgpt_nvim/tools/replace_in_file.lua | 13 ++++++- 6 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 lua/chatgpt_nvim/tools/lint.lua diff --git a/README.md b/README.md index dfb19be..e613435 100644 --- a/README.md +++ b/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 +``` + diff --git a/lua/chatgpt_nvim/config.lua b/lua/chatgpt_nvim/config.lua index 4c47492..1dea16a 100644 --- a/lua/chatgpt_nvim/config.lua +++ b/lua/chatgpt_nvim/config.lua @@ -51,7 +51,9 @@ function M.load() enable_chunking = false, enable_step_by_step = true, - -- Removed enable_debug_commands + -- If auto_lint is true, we'll run a quick linter after editing or replacing files. + auto_lint = false, + tool_auto_accept = { readFile = false, editFile = false, @@ -107,7 +109,11 @@ function M.load() config.enable_step_by_step = result.enable_step_by_step end - -- Load tool_auto_accept if present + -- New: load auto_lint if present + if type(result.auto_lint) == "boolean" then + config.auto_lint = result.auto_lint + end + 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 diff --git a/lua/chatgpt_nvim/tools/edit_file.lua b/lua/chatgpt_nvim/tools/edit_file.lua index c126a59..9de8c5a 100644 --- a/lua/chatgpt_nvim/tools/edit_file.lua +++ b/lua/chatgpt_nvim/tools/edit_file.lua @@ -1,4 +1,6 @@ local handler = require("chatgpt_nvim.handler") +local lint = require("chatgpt_nvim.tools.lint") + local M = {} M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file) @@ -14,13 +16,26 @@ 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("\n%s\n", 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) Lint check if enabled + if conf.auto_lint then + local output, err = lint.lint_file(path) + if output then + msg[#msg+1] = "\n--- Lint Results ---\n" + msg[#msg+1] = output + else + msg[#msg+1] = "\n(Lint) " .. (err or "Could not lint this file.") + end + end + return table.concat(msg, "") end diff --git a/lua/chatgpt_nvim/tools/lint.lua b/lua/chatgpt_nvim/tools/lint.lua new file mode 100644 index 0000000..8b1613b --- /dev/null +++ b/lua/chatgpt_nvim/tools/lint.lua @@ -0,0 +1,42 @@ +local uv = vim.loop + +local M = {} + +-- A naive lint approach: if a file ends with .lua, run "luacheck" if available. +-- If .go, run "go build" or "go vet", etc. This is just an example; adapt to your needs. +-- Return nil if lint tool can't be determined or is not installed. +local function guess_linter_command(path) + if path:match("%.lua$") then + return "luacheck " .. vim.fn.shellescape(path) + elseif path:match("%.go$") then + -- We'll just do a quick "go build" or "go vet" + return "go vet " .. vim.fn.fnamemodify(path, ":h") + end + return nil +end + +-- Executes the linter command and returns the output or an error if it fails to run. +local function run_command(cmd) + local handle = io.popen(cmd) + if not handle then + return nil, ("Failed to run: %s"):format(cmd) + end + local output = handle:read("*a") or "" + handle:close() + return output, nil +end + +function M.lint_file(path) + local lint_cmd = guess_linter_command(path) + if not lint_cmd then + return nil, "No known lint command for file: " .. path + end + + local output, err = run_command(lint_cmd) + if not output then + return nil, err or ("Failed to lint: " .. path) + end + return output, nil +end + +return M diff --git a/lua/chatgpt_nvim/tools/manager.lua b/lua/chatgpt_nvim/tools/manager.lua index 29af448..50d4f62 100644 --- a/lua/chatgpt_nvim/tools/manager.lua +++ b/lua/chatgpt_nvim/tools/manager.lua @@ -14,42 +14,55 @@ local function is_destructive_command(cmd) end 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 local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn) local messages = {} + for _, call in ipairs(tools) do 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 @@ -57,7 +70,7 @@ local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn) 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 ("..#combined.."). Please break down the operations into smaller steps." + return ("The combined tool output is too large (%d chars). Please break down the operations into smaller steps."):format(#combined) end return combined end diff --git a/lua/chatgpt_nvim/tools/replace_in_file.lua b/lua/chatgpt_nvim/tools/replace_in_file.lua index 4059a5e..85b03b1 100644 --- a/lua/chatgpt_nvim/tools/replace_in_file.lua +++ b/lua/chatgpt_nvim/tools/replace_in_file.lua @@ -1,4 +1,5 @@ local handler = require("chatgpt_nvim.handler") +local lint = require("chatgpt_nvim.tools.lint") local M = {} @@ -8,7 +9,6 @@ local function search_and_replace(original, replacements) 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 +41,17 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file msg[#msg+1] = string.format("\n%s\n", 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" + -- 2) Lint check if enabled + if conf.auto_lint then + local output, err = lint.lint_file(path) + if output then + msg[#msg+1] = "\n--- Lint Results ---\n" + msg[#msg+1] = output + else + msg[#msg+1] = "\n(Lint) " .. (err or "Could not lint this file.") + end + end + return table.concat(msg, "") end