diff --git a/.chatgpt_config.yaml b/.chatgpt_config.yaml index 3d14460..d0ad42b 100644 --- a/.chatgpt_config.yaml +++ b/.chatgpt_config.yaml @@ -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: diff --git a/lua/chatgpt_nvim/config.lua b/lua/chatgpt_nvim/config.lua index 1dea16a..2bd541f 100644 --- a/lua/chatgpt_nvim/config.lua +++ b/lua/chatgpt_nvim/config.lua @@ -51,7 +51,7 @@ function M.load() enable_chunking = false, enable_step_by_step = true, - -- If auto_lint is true, we'll run a quick linter after editing or replacing files. + -- If auto_lint is true, we only do LSP-based checks. auto_lint = false, tool_auto_accept = { @@ -109,7 +109,7 @@ function M.load() config.enable_step_by_step = result.enable_step_by_step end - -- New: load auto_lint if present + -- auto_lint controls whether we do LSP-based checks if type(result.auto_lint) == "boolean" then config.auto_lint = result.auto_lint end diff --git a/lua/chatgpt_nvim/tools/edit_file.lua b/lua/chatgpt_nvim/tools/edit_file.lua index 9de8c5a..35fdb3f 100644 --- a/lua/chatgpt_nvim/tools/edit_file.lua +++ b/lua/chatgpt_nvim/tools/edit_file.lua @@ -1,5 +1,5 @@ local handler = require("chatgpt_nvim.handler") -local lint = require("chatgpt_nvim.tools.lint") +local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics") local M = {} @@ -25,15 +25,10 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file 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 + -- 2) If auto_lint => run robust LSP approach 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 + 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, "") diff --git a/lua/chatgpt_nvim/tools/lint.lua b/lua/chatgpt_nvim/tools/lint.lua deleted file mode 100644 index 8b1613b..0000000 --- a/lua/chatgpt_nvim/tools/lint.lua +++ /dev/null @@ -1,42 +0,0 @@ -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/lsp_robust_diagnostics.lua b/lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua new file mode 100644 index 0000000..70c723d --- /dev/null +++ b/lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua @@ -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 diff --git a/lua/chatgpt_nvim/tools/replace_in_file.lua b/lua/chatgpt_nvim/tools/replace_in_file.lua index 85b03b1..976f360 100644 --- a/lua/chatgpt_nvim/tools/replace_in_file.lua +++ b/lua/chatgpt_nvim/tools/replace_in_file.lua @@ -1,10 +1,9 @@ local handler = require("chatgpt_nvim.handler") -local lint = require("chatgpt_nvim.tools.lint") +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 "" @@ -41,15 +40,9 @@ 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 + local diag_str = robust_lsp.lsp_check_file_content(path, updated_data, 3000) + msg[#msg+1] = "\n" .. diag_str end return table.concat(msg, "")