feat: add lint tool
This commit is contained in:
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.
|
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!
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ function M.load()
|
|||||||
enable_chunking = false,
|
enable_chunking = false,
|
||||||
enable_step_by_step = true,
|
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 = {
|
tool_auto_accept = {
|
||||||
readFile = false,
|
readFile = false,
|
||||||
editFile = false,
|
editFile = false,
|
||||||
@@ -107,7 +109,11 @@ function M.load()
|
|||||||
config.enable_step_by_step = result.enable_step_by_step
|
config.enable_step_by_step = result.enable_step_by_step
|
||||||
end
|
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
|
if type(result.tool_auto_accept) == "table" then
|
||||||
for k, v in pairs(result.tool_auto_accept) do
|
for k, v in pairs(result.tool_auto_accept) do
|
||||||
if config.tool_auto_accept[k] ~= nil and type(v) == "boolean" then
|
if config.tool_auto_accept[k] ~= nil and type(v) == "boolean" then
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
local handler = require("chatgpt_nvim.handler")
|
local handler = require("chatgpt_nvim.handler")
|
||||||
|
local lint = require("chatgpt_nvim.tools.lint")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
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)
|
return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 1) Write the new content
|
||||||
handler.write_file(path, new_content, conf)
|
handler.write_file(path, new_content, conf)
|
||||||
|
|
||||||
local msg = {}
|
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] = 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] = "\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] = 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"
|
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, "")
|
return table.concat(msg, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
42
lua/chatgpt_nvim/tools/lint.lua
Normal file
42
lua/chatgpt_nvim/tools/lint.lua
Normal file
@@ -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
|
||||||
@@ -14,42 +14,55 @@ local function is_destructive_command(cmd)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function prompt_user_tool_accept(tool_call, conf)
|
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]
|
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 tool_call.tool == "executeCommand" and auto_accept then
|
||||||
if is_destructive_command(tool_call.command) then
|
if is_destructive_command(tool_call.command) then
|
||||||
auto_accept = false
|
auto_accept = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not auto_accept then
|
if auto_accept then
|
||||||
return ask_user(("Tool request: %s -> Accept?"):format(tool_call.tool or "unknown"))
|
-- If auto-accepted and not destructive, no prompt needed
|
||||||
else
|
|
||||||
return true
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
|
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
|
||||||
local messages = {}
|
local messages = {}
|
||||||
|
|
||||||
for _, call in ipairs(tools) do
|
for _, call in ipairs(tools) do
|
||||||
local accepted = prompt_user_tool_accept(call, conf)
|
local accepted = prompt_user_tool_accept(call, conf)
|
||||||
if not accepted then
|
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
|
else
|
||||||
local tool_impl = tools_module.tools_by_name[call.tool]
|
local tool_impl = tools_module.tools_by_name[call.tool]
|
||||||
if tool_impl then
|
if tool_impl then
|
||||||
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
|
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
|
||||||
table.insert(messages, msg)
|
table.insert(messages, msg)
|
||||||
else
|
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
|
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 combined = table.concat(messages, "\n\n")
|
||||||
local limit = conf.prompt_char_limit or 8000
|
local limit = conf.prompt_char_limit or 8000
|
||||||
if #combined > limit then
|
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
|
end
|
||||||
return combined
|
return combined
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local handler = require("chatgpt_nvim.handler")
|
local handler = require("chatgpt_nvim.handler")
|
||||||
|
local lint = require("chatgpt_nvim.tools.lint")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ local function search_and_replace(original, replacements)
|
|||||||
for _, r in ipairs(replacements) do
|
for _, r in ipairs(replacements) do
|
||||||
local search_str = r.search or ""
|
local search_str = r.search or ""
|
||||||
local replace_str = r.replace or ""
|
local replace_str = r.replace or ""
|
||||||
-- Here we do a global plain text replacement
|
|
||||||
updated = updated:gsub(search_str, replace_str)
|
updated = updated:gsub(search_str, replace_str)
|
||||||
end
|
end
|
||||||
return updated
|
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("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data)
|
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"
|
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, "")
|
return table.concat(msg, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user