diff --git a/.chatgpt_config.yaml b/.chatgpt_config.yaml index d0ad42b..c34dc1a 100644 --- a/.chatgpt_config.yaml +++ b/.chatgpt_config.yaml @@ -1,27 +1,17 @@ project_name: "chatgpt_nvim" default_prompt_blocks: - "basic-prompt" - - "secure-coding" + - "workflow-prompt" +directories: + - "." initial_files: - "README.md" - debug: false -improved_debug: false - preview_changes: false interactive_file_selection: false partial_acceptance: false - +improved_debug: false enable_debug_commands: true -prompt_char_limit: 300000 +token_limit: 128000 enable_chunking: false enable_step_by_step: true -auto_lint: true - -# New tool auto-accept config -tool_auto_accept: - readFile: false - editFile: false - executeCommand: false - # If you set any of these to true, it will auto accept them without prompting. - # 'executeCommand' should remain false by default unless you're certain it's safe. diff --git a/README.md b/README.md index e613435..dfb19be 100644 --- a/README.md +++ b/README.md @@ -83,18 +83,3 @@ 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 2bd541f..e239044 100644 --- a/lua/chatgpt_nvim/config.lua +++ b/lua/chatgpt_nvim/config.lua @@ -40,6 +40,7 @@ function M.load() initial_prompt = "", directories = { "." }, default_prompt_blocks = {}, + -- Changed default from 128000 to 16384 as requested prompt_char_limit = 300000, project_name = "", debug = false, @@ -49,17 +50,9 @@ function M.load() partial_acceptance = false, improved_debug = false, enable_chunking = false, + -- New default for step-by-step enable_step_by_step = true, - - -- 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, - } + enable_debug_commands = false } if fd then @@ -105,21 +98,12 @@ function M.load() if type(result.enable_chunking) == "boolean" then config.enable_chunking = result.enable_chunking end + -- Added logic to load enable_step_by_step from user config if type(result.enable_step_by_step) == "boolean" then config.enable_step_by_step = result.enable_step_by_step end - - -- auto_lint controls whether we do LSP-based checks - 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 - config.tool_auto_accept[k] = v - end - end + if type(result.enable_debug_commands) == "boolean" then + config.enable_debug_commands = result.enable_debug_commands end end end @@ -127,7 +111,7 @@ function M.load() config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..." end - -- Merge default prompt blocks + -- Merge the default prompt blocks with the config's initial prompt 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 diff --git a/lua/chatgpt_nvim/init.lua b/lua/chatgpt_nvim/init.lua index 2ea41c3..8a7ab88 100644 --- a/lua/chatgpt_nvim/init.lua +++ b/lua/chatgpt_nvim/init.lua @@ -6,14 +6,8 @@ local config = require('chatgpt_nvim.config') local ui = require('chatgpt_nvim.ui') local prompts = require('chatgpt_nvim.prompts') -local tools_manager = require("chatgpt_nvim.tools.manager") -local tools_module = require("chatgpt_nvim.tools") - local ok_yaml, lyaml = pcall(require, "lyaml") ------------------------------------------------------------------------------- --- UTILITIES ------------------------------------------------------------------------------- local function copy_to_clipboard(text) vim.fn.setreg('+', text) end @@ -54,6 +48,19 @@ local function read_file(path) return data end +local function is_directory(path) + local stat = vim.loop.fs_stat(path) + return stat and stat.type == "directory" +end + +local function handle_step_by_step_if_needed(prompt, conf) + local length = #prompt + if not conf.enable_step_by_step or length <= (conf.prompt_char_limit or 8000) then + return { prompt } + end + return { prompts["step-prompt"] } +end + local function close_existing_buffer_by_name(pattern) for _, b in ipairs(vim.api.nvim_list_bufs()) do local name = vim.api.nvim_buf_get_name(b) @@ -63,98 +70,265 @@ local function close_existing_buffer_by_name(pattern) end end ------------------------------------------------------------------------------- --- PROMPT CONSTRUCTION ------------------------------------------------------------------------------- -local function build_tools_available_block() - local lines = {} - lines[#lines+1] = "" - for _, t in ipairs(tools_module.available_tools) do - lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s", - t.name, t.explanation, t.usage - ) - end - lines[#lines+1] = "" - return table.concat(lines, "\n") -end - -local function build_prompt(user_input, dirs, conf) - local root = vim.fn.getcwd() - local initial_files = conf.initial_files or {} - local final_sections = {} - - -- 1) - if conf.initial_prompt and conf.initial_prompt ~= "" then - table.insert(final_sections, "\n" .. conf.initial_prompt .. "\n\n") - end - - -- 2) - table.insert(final_sections, build_tools_available_block()) - - -- 3) - local task_lines = {} - task_lines[#task_lines+1] = "" - 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) - end - task_lines[#task_lines+1] = "\n" - table.insert(final_sections, table.concat(task_lines, "\n")) - - -- 4) - local file_content_blocks = {} - for _, file_path in ipairs(initial_files) do - local full_path = root .. "/" .. file_path - if is_subpath(root, full_path) then - local fdata = read_file(full_path) - if fdata then - file_content_blocks[#file_content_blocks+1] = string.format( - "\n%s\n", file_path, fdata - ) +local function preview_changes(changes, conf) + close_existing_buffer_by_name("ChatGPT_Changes_Preview$") + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Changes_Preview") + vim.api.nvim_buf_set_option(bufnr, "filetype", "diff") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "# Preview of Changes:", + "# (Close this window to apply changes or use :q to cancel)", + "" + }) + for _, fileinfo in ipairs(changes) do + local indicator = (fileinfo.delete == true) and "Delete file" or "Write file" + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { + string.format("=== %s: %s ===", indicator, fileinfo.path or "") + }) + if fileinfo.content then + local lines = vim.split(fileinfo.content, "\n") + for _, line in ipairs(lines) do + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line }) end end - end - if #file_content_blocks > 0 then - table.insert(final_sections, table.concat(file_content_blocks, "\n\n")) + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" }) end - -- 5) - local env_lines = {} - env_lines[#env_lines+1] = "" - env_lines[#env_lines+1] = "# VSCode Visible Files" - for _, f in ipairs(initial_files) do - env_lines[#env_lines+1] = f - end - env_lines[#env_lines+1] = "" - env_lines[#env_lines+1] = "# VSCode Open Tabs" - env_lines[#env_lines+1] = "..." - env_lines[#env_lines+1] = "" - env_lines[#env_lines+1] = "# Current Time" - env_lines[#env_lines+1] = os.date("%x, %X (%Z)") - env_lines[#env_lines+1] = "" - env_lines[#env_lines+1] = "# Current Working Directory (" .. root .. ") Files" - env_lines[#env_lines+1] = context.get_project_structure(dirs, conf) or "" - env_lines[#env_lines+1] = "" - env_lines[#env_lines+1] = "# Current Mode" - env_lines[#env_lines+1] = "ACT MODE" - env_lines[#env_lines+1] = "" - table.insert(final_sections, table.concat(env_lines, "\n")) - - return table.concat(final_sections, "\n\n") + vim.cmd("vsplit") + vim.cmd("buffer " .. bufnr) end -local function handle_step_by_step_if_needed(prompt, conf) - local length = #prompt - local limit = conf.prompt_char_limit or 8000 - if (not conf.enable_step_by_step) or (length <= limit) then - return { prompt } +local function partial_accept(changes, conf) + close_existing_buffer_by_name("ChatGPT_Partial_Accept$") + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Partial_Accept") + vim.api.nvim_buf_set_option(bufnr, "filetype", "diff") + + local lines = { + "# Remove or comment out (prepend '#') any changes you do NOT want, then :wq, :x, or :bd to finalize partial acceptance", + "" + } + for _, fileinfo in ipairs(changes) do + local action = (fileinfo.delete == true) and "[DELETE]" or "[WRITE]" + table.insert(lines, string.format("%s %s", action, fileinfo.path or "")) + if fileinfo.content then + local content_lines = vim.split(fileinfo.content, "\n") + for _, cl in ipairs(content_lines) do + table.insert(lines, " " .. cl) + end + end + table.insert(lines, "") + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + local final_changes = {} + local function on_write() + local edited_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local keep_current = false + local current_fileinfo = { path = nil, content = nil, delete = false } + local content_accum = {} + + for _, line in ipairs(edited_lines) do + if line:match("^#") or line == "" then + goto continue + end + local del_match = line:match("^%[DELETE%] (.+)") + local write_match = line:match("^%[WRITE%] (.+)") + if del_match then + if keep_current and (current_fileinfo.path ~= nil) then + if #content_accum > 0 then + current_fileinfo.content = table.concat(content_accum, "\n") + end + table.insert(final_changes, current_fileinfo) + end + keep_current = true + current_fileinfo = { path = del_match, delete = true, content = nil } + content_accum = {} + elseif write_match then + if keep_current and (current_fileinfo.path ~= nil) then + if #content_accum > 0 then + current_fileinfo.content = table.concat(content_accum, "\n") + end + table.insert(final_changes, current_fileinfo) + end + keep_current = true + current_fileinfo = { path = write_match, delete = false, content = nil } + content_accum = {} + else + if keep_current then + table.insert(content_accum, line:gsub("^%s*", "")) + end + end + ::continue:: + end + + if keep_current and (current_fileinfo.path ~= nil) then + if #content_accum > 0 then + current_fileinfo.content = table.concat(content_accum, "\n") + end + table.insert(final_changes, current_fileinfo) + end + + vim.api.nvim_buf_set_option(bufnr, "modified", false) + end + + vim.api.nvim_create_autocmd("BufWriteCmd", { + buffer = bufnr, + once = true, + callback = function() + on_write() + vim.cmd("bd! " .. bufnr) + end + }) + + vim.cmd("split") + vim.cmd("buffer " .. bufnr) + + vim.wait(60000, function() + local winids = vim.api.nvim_tabpage_list_wins(0) + for _, w in ipairs(winids) do + local b = vim.api.nvim_win_get_buf(w) + if b == bufnr then + return false + end + end + return true + end) + + return final_changes +end + +local function store_prompt_for_reference(prompt, conf) + close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Generated_Prompt") + vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown") + + local lines = { + "# Below is the generated prompt. You can keep it for reference:", + "" + } + local prompt_lines = vim.split(prompt, "\n") + for _, line in ipairs(prompt_lines) do + table.insert(lines, line) + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.cmd("vsplit") + vim.cmd("buffer " .. bufnr) +end + +-- Updated to allow flags/args for 'ls' and 'grep' +local function execute_debug_command(cmd, conf) + if type(cmd) ~= "table" or not cmd.command then + return "Invalid command object." + end + + local command = cmd.command + if command == "ls" then + -- Accept optional flags/args + 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 + -- Accept optional flags/args + -- If the user wants to do something like: + -- args: ["-r", "somePattern", "someDir"] + -- we just pass them all to grep. + local args = cmd.args or {} + if #args == 0 then + -- fallback to old usage + 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='', target=''}" + end + local stat = vim.loop.fs_stat(target) + if not stat then + return "Cannot grep: target path does not exist" + end + -- old logic remains for backward compatibility + if stat.type == "directory" then + local handle = io.popen("ls -p " .. target .. " | grep -v /") + if not handle then + return "Failed to read directory contents for grep." + end + local all_files = {} + for file in handle:read("*a"):gmatch("[^\n]+") do + table.insert(all_files, target .. "/" .. file) + end + handle:close() + local results = {} + local function grep_in_file(search_string, filepath) + local content = read_file(filepath) + if not content then + return "Could not read file: " .. filepath + end + local lines = {} + local line_num = 0 + for line in content:gmatch("([^\n]*)\n?") do + line_num = line_num + 1 + if line:find(search_string, 1, true) then + table.insert(lines, filepath .. ":" .. line_num .. ":" .. line) + end + end + return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n") + end + for _, f in ipairs(all_files) do + local fstat = vim.loop.fs_stat(f) + if fstat and fstat.type == "file" then + table.insert(results, grep_in_file(pattern, f)) + end + end + return table.concat(results, "\n") + else + local function grep_in_file(search_string, filepath) + local content = read_file(filepath) + if not content then + return "Could not read file: " .. filepath + end + local lines = {} + local line_num = 0 + for line in content:gmatch("([^\n]*)\n?") do + line_num = line_num + 1 + if line:find(search_string, 1, true) then + table.insert(lines, filepath .. ":" .. line_num .. ":" .. line) + end + end + return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n") + end + return grep_in_file(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 - return { prompts["step-prompt"] } end ------------------------------------------------------------------------------- --- :ChatGPT ------------------------------------------------------------------------------- local function run_chatgpt_command() local conf = config.load() ui.setup_ui(conf) @@ -176,7 +350,7 @@ local function run_chatgpt_command() vim.api.nvim_buf_set_option(bufnr, "modifiable", true) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { - "# Enter your main user prompt (task) below.", + "# Enter your prompt below.", "", "Save & close with :wq, :x, or :bd to finalize your prompt." }) @@ -186,25 +360,57 @@ local function run_chatgpt_command() callback = function() local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local user_input = table.concat(lines, "\n") - if user_input == "" or user_input:find("^# Enter your main user prompt %(task%) below.") then + if user_input == "" or user_input:find("^# Enter your prompt below.") then vim.api.nvim_out_write("No valid input provided.\n") vim.api.nvim_buf_set_option(bufnr, "modified", false) return end - local final_prompt = build_prompt(user_input, dirs, conf) - local chunks = handle_step_by_step_if_needed(final_prompt, conf) + local project_structure = context.get_project_structure(dirs, conf) + local initial_files = conf.initial_files or {} + local included_sections = {} - close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") - local bufnr_ref = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") - vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") + for _, item in ipairs(initial_files) do + local root = vim.fn.getcwd() + local full_path = root .. "/" .. item + if is_directory(full_path) then + local dir_files = context.get_project_files({item}, conf) + for _, f in ipairs(dir_files) do + local path = root .. "/" .. f + local data = read_file(path) + if data then + table.insert(included_sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n") + end + end + else + local data = read_file(full_path) + if data then + table.insert(included_sections, "\nFile: `" .. item .. "`\n```\n" .. data .. "\n```\n") + end + end + end - local prompt_lines = vim.split(chunks[1], "\n") - vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) - vim.cmd("vsplit") - vim.cmd("buffer " .. bufnr_ref) + local initial_sections = { + "## Basic Prompt Instructions:\n", + conf.initial_prompt .. "\n\n\n", + "## User Instructions:\n", + user_input .. "\n\n\n", + "## Context/Data:\n", + "Project name: " .. (conf.project_name or "") .. "\n", + "Project Structure:\n", + project_structure, + table.concat(included_sections, "\n") + } + if conf.enable_debug_commands then + table.insert(initial_sections, "\n## Debug Commands Info:\n") + table.insert(initial_sections, prompts["debug-commands-info"]) + end + + local prompt = table.concat(initial_sections, "\n") + store_prompt_for_reference(prompt, conf) + + local chunks = handle_step_by_step_if_needed(prompt, conf) copy_to_clipboard(chunks[1]) if #chunks == 1 then vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n") @@ -219,9 +425,6 @@ local function run_chatgpt_command() vim.cmd("buffer " .. bufnr) end ------------------------------------------------------------------------------- --- :ChatGPTPaste ------------------------------------------------------------------------------- local function run_chatgpt_paste_command() local conf = config.load() ui.setup_ui(conf) @@ -238,22 +441,19 @@ local function run_chatgpt_paste_command() return end - -- 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 + if data.commands and conf.enable_debug_commands then + local results = {} + for _, cmd in ipairs(data.commands) do + table.insert(results, execute_debug_command(cmd, conf)) end - - local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file) - copy_to_clipboard(output_messages) - print("Tool call results have been processed and copied to clipboard.") + local output = table.concat(results, "\n\n") + copy_to_clipboard(output) + print("Debug command results copied to clipboard!") return end - -- 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 + ui.debug_log("Received project_name and files in response.") if data.project_name ~= conf.project_name then vim.api.nvim_err_writeln("Project name mismatch. Aborting.") return @@ -269,7 +469,7 @@ local function run_chatgpt_paste_command() if is_final then if conf.preview_changes then - require('chatgpt_nvim.init').preview_changes(data.files, conf) + preview_changes(data.files, conf) print("Close the preview window to apply changes, or use :q to cancel.") local closed = vim.wait(60000, function() local bufs = vim.api.nvim_list_bufs() @@ -289,7 +489,7 @@ local function run_chatgpt_paste_command() local final_files = data.files if conf.partial_acceptance then - final_files = require('chatgpt_nvim.init').partial_accept(data.files, conf) + final_files = partial_accept(data.files, conf) if #final_files == 0 then vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.") return @@ -300,28 +500,30 @@ local function run_chatgpt_paste_command() for _, fileinfo in ipairs(final_files) do if not fileinfo.path then vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.") - else - if not is_subpath(root, fileinfo.path) then - vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path) - else - if fileinfo.delete == true then - ui.debug_log("Deleting file: " .. fileinfo.path) - handler.delete_file(fileinfo.path, conf) - print("Deleted: " .. fileinfo.path) - elseif fileinfo.content then - ui.debug_log("Writing file: " .. fileinfo.path) - handler.write_file(fileinfo.path, fileinfo.content, conf) - print("Wrote: " .. fileinfo.path) - else - vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.") - end - end + goto continue end + + if not is_subpath(root, fileinfo.path) then + vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path) + goto continue + end + + if fileinfo.delete == true then + ui.debug_log("Deleting file: " .. fileinfo.path) + handler.delete_file(fileinfo.path, conf) + print("Deleted: " .. fileinfo.path) + elseif fileinfo.content then + ui.debug_log("Writing file: " .. fileinfo.path) + handler.write_file(fileinfo.path, fileinfo.content, conf) + print("Wrote: " .. fileinfo.path) + else + vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.") + end + ::continue:: end + else - -- Not final => user is requesting more files local requested_paths = {} - local root = vim.fn.getcwd() for _, fileinfo in ipairs(data.files) do if fileinfo.path then table.insert(requested_paths, fileinfo.path) @@ -329,13 +531,14 @@ local function run_chatgpt_paste_command() end local file_sections = {} + local root = vim.fn.getcwd() for _, f in ipairs(requested_paths) do local path = root .. "/" .. f local content = read_file(path) if content then - table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content)) + table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n" .. content .. "\n```\n") else - table.insert(file_sections, ("\nFile: `%s`\n```\n(Could not read file)\n```\n"):format(f)) + table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n(Could not read file)\n```\n") end end @@ -344,7 +547,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 'tools:' approach. If you have all info, provide final changes or continue your instructions." + "\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (with `content` or `delete`) to apply changes.\n" } local prompt = table.concat(sections, "\n") @@ -362,21 +565,17 @@ local function run_chatgpt_paste_command() end copy_to_clipboard(prompt) - print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.") + print("Prompt (with requested files) copied to clipboard! Paste it into the ChatGPT O1 model.") end else - vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.") + vim.api.nvim_err_writeln("Invalid response. Expected 'project_name' and 'files'.") end end ------------------------------------------------------------------------------- --- :ChatGPTCurrentBuffer ------------------------------------------------------------------------------- local function run_chatgpt_current_buffer_command() local conf = config.load() ui.setup_ui(conf) ui.debug_log("Running :ChatGPTCurrentBuffer command.") - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local user_input = table.concat(lines, "\n") local dirs = conf.directories or {"."} @@ -387,19 +586,92 @@ local function run_chatgpt_current_buffer_command() end end - local final_prompt = build_prompt(user_input, dirs, conf) - local chunks = handle_step_by_step_if_needed(final_prompt, conf) + local project_structure = context.get_project_structure(dirs, conf) + local initial_files = conf.initial_files or {} + local included_sections = {} - close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") - local bufnr_ref = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") - vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") + local function is_directory(path) + local stat = vim.loop.fs_stat(path) + return stat and stat.type == "directory" + end - local prompt_lines = vim.split(chunks[1], "\n") - vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) - vim.cmd("vsplit") - vim.cmd("buffer " .. bufnr_ref) + local function read_file(path) + local fd = vim.loop.fs_open(path, "r", 438) + if not fd then + return nil + end + local stat = vim.loop.fs_fstat(fd) + if not stat then + vim.loop.fs_close(fd) + return nil + end + local data = vim.loop.fs_read(fd, stat.size, 0) + vim.loop.fs_close(fd) + return data + end + for _, item in ipairs(initial_files) do + local root = vim.fn.getcwd() + local full_path = root .. "/" .. item + if is_directory(full_path) then + local dir_files = context.get_project_files({item}, conf) + for _, f in ipairs(dir_files) do + local path = root .. "/" .. f + local data = read_file(path) + if data then + table.insert(included_sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n") + end + end + else + local data = read_file(full_path) + if data then + table.insert(included_sections, "\nFile: `" .. item .. "`\n```\n" .. data .. "\n```\n") + end + end + end + + local initial_sections = { + "## Basic Prompt Instructions:\n", + conf.initial_prompt .. "\n\n\n", + "## User Instructions:\n", + user_input .. "\n\n\n", + "## Context/Data:\n", + "Project name: " .. (conf.project_name or "") .. "\n", + "Project Structure:\n", + project_structure, + table.concat(included_sections, "\n") + } + + if conf.enable_debug_commands then + table.insert(initial_sections, "\n## Debug Commands Info:\n") + table.insert(initial_sections, prompts["debug-commands-info"]) + end + + local prompt = table.concat(initial_sections, "\n") + + local function store_prompt_for_reference(pr) + close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") + local bufnr_ref = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") + vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") + + local lines_ref = { + "# Below is the generated prompt. You can keep it for reference:", + "" + } + local pr_lines = vim.split(pr, "\n") + for _, line in ipairs(pr_lines) do + table.insert(lines_ref, line) + end + + vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, lines_ref) + vim.cmd("vsplit") + vim.cmd("buffer " .. bufnr_ref) + end + + store_prompt_for_reference(prompt, conf) + + local chunks = handle_step_by_step_if_needed(prompt, conf) copy_to_clipboard(chunks[1]) if #chunks == 1 then vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n") @@ -408,11 +680,9 @@ local function run_chatgpt_current_buffer_command() end end ------------------------------------------------------------------------------- --- PUBLIC API ------------------------------------------------------------------------------- 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 +M.execute_debug_command = execute_debug_command return M diff --git a/lua/chatgpt_nvim/prompts.lua b/lua/chatgpt_nvim/prompts.lua index 941ddab..9e00cd5 100644 --- a/lua/chatgpt_nvim/prompts.lua +++ b/lua/chatgpt_nvim/prompts.lua @@ -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,41 +256,54 @@ local M = { ["basic-prompt"] = [[ ### Basic Prompt - 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: + 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. - 1. `project_name: ""` (matching the real project name) - 2. A `tools:` array describing the operations you want to perform. + 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. - **Example** (substitute `` with the real name): - ```yaml - project_name: "my_project" - tools: - - tool: "readFile" - path: "relative/path/to/file" + 3. **Provide Output YAML** + - When you have all information, output the final YAML in this format: + ```yaml + project_name: my_project - - tool: "replace_in_file" - path: "relative/path/to/file" - replacements: - - search: "old text" - replace: "new text" + files: + - path: "src/main.py" + content: | + # Updated main function + def main(): + print("Hello, World!") - - tool: "editFile" - path: "relative/path/to/file" - content: | - # Full updated file content here + - 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: "executeCommand" - command: "ls -la" - ``` + 4. **Explain Changes** + - After the final YAML, add a brief explanation of the modifications (outside the YAML). - **Key Points**: - - Always include `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 in the `tools` array. - - Always run at least one tool (e.g., `readFile`, `editFile`, `executeCommand`), exept you have finished. - - Always just include one yaml in the response with all the tools you want to run in that yaml. - - 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. + 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. ]], ["secure-coding"] = [[ ### Secure Coding Guidelines @@ -357,10 +370,33 @@ 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. ]] } diff --git a/lua/chatgpt_nvim/tools/edit_file.lua b/lua/chatgpt_nvim/tools/edit_file.lua deleted file mode 100644 index 35fdb3f..0000000 --- a/lua/chatgpt_nvim/tools/edit_file.lua +++ /dev/null @@ -1,37 +0,0 @@ -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) - local path = tool_call.path - local new_content = tool_call.content - - if not path or not new_content then - return "[edit_file] Missing 'path' or 'content'." - end - local root = vim.fn.getcwd() - - if not is_subpath(root, path) then - 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) 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 - -return M diff --git a/lua/chatgpt_nvim/tools/execute_command.lua b/lua/chatgpt_nvim/tools/execute_command.lua deleted file mode 100644 index 5fb9274..0000000 --- a/lua/chatgpt_nvim/tools/execute_command.lua +++ /dev/null @@ -1,17 +0,0 @@ -local M = {} - -M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file) - local cmd = tool_call.command - if not cmd then - return "[executeCommand] Missing 'command'." - end - local handle = io.popen(cmd) - if not handle then - return string.format("Tool [executeCommand '%s'] FAILED to popen.", cmd) - end - local result = handle:read("*a") or "" - handle:close() - return string.format("Tool [executeCommand '%s'] Result:\n%s", cmd, result) -end - -return M diff --git a/lua/chatgpt_nvim/tools/init.lua b/lua/chatgpt_nvim/tools/init.lua deleted file mode 100644 index 48383dd..0000000 --- a/lua/chatgpt_nvim/tools/init.lua +++ /dev/null @@ -1,39 +0,0 @@ -local read_file_tool = require("chatgpt_nvim.tools.read_file") -local edit_file_tool = require("chatgpt_nvim.tools.edit_file") -local replace_in_file_tool = require("chatgpt_nvim.tools.replace_in_file") -local execute_command_tool = require("chatgpt_nvim.tools.execute_command") - -local M = {} - --- We can store a table of available tools here -M.available_tools = { - { - name = "readFile", - usage = "Retrieve the contents of a file. Provide { tool='readFile', path='...' }", - explanation = "Use this to read file content directly from the disk." - }, - { - name = "editFile", - usage = "Overwrite an entire file's content. Provide { tool='editFile', path='...', content='...' }", - explanation = "Use this when you want to replace a file with new content." - }, - { - name = "replace_in_file", - usage = "Perform a search-and-replace. Provide { tool='replace_in_file', path='...', replacements=[ { search='...', replace='...' }, ... ] }", - explanation = "Use this to apply incremental changes without fully overwriting the file." - }, - { - name = "executeCommand", - usage = "Run a shell command. Provide { tool='executeCommand', command='...' }", - explanation = "Use with caution, especially for destructive operations (rm, sudo, etc.)." - }, -} - -M.tools_by_name = { - readFile = read_file_tool, - editFile = edit_file_tool, - replace_in_file = replace_in_file_tool, - executeCommand = execute_command_tool -} - -return M diff --git a/lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua b/lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua deleted file mode 100644 index 8693c2b..0000000 --- a/lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua +++ /dev/null @@ -1,177 +0,0 @@ -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) - -- Create a unique buffer name so we never clash with an existing one - local scratch_name = string.format("chatgpt-scratch://%s#%d", path, math.random(100000, 999999)) - - local bufnr = api.nvim_create_buf(false, true) - if bufnr == 0 then - return nil - end - - -- Assign the unique name to the buffer - api.nvim_buf_set_name(bufnr, scratch_name) - - -- Convert content string to lines - 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) - - -- Mark it as a scratch buffer - 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") - -- Even though the buffer name is unique, the LSP server sees this file as if at 'path' - 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 - 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 - api.nvim_buf_delete(bufnr, { force = true }) - return "(LSP) " .. err2 - end - - -- Optionally do a didChange - send_did_change(bufnr, client_id) - - local diags = wait_for_diagnostics(bufnr, timeout_ms or 2000) - local diag_str = diagnostics_to_string(diags) - - api.nvim_buf_delete(bufnr, { force = true }) - return diag_str -end - -return M diff --git a/lua/chatgpt_nvim/tools/manager.lua b/lua/chatgpt_nvim/tools/manager.lua deleted file mode 100644 index 50d4f62..0000000 --- a/lua/chatgpt_nvim/tools/manager.lua +++ /dev/null @@ -1,79 +0,0 @@ -local tools_module = require("chatgpt_nvim.tools") - -local M = {} - -local function is_destructive_command(cmd) - if not cmd then return false end - local destructive_list = { "rm", "sudo", "mv", "cp" } - for _, keyword in ipairs(destructive_list) do - if cmd:match("(^" .. keyword .. "[%s$])") or cmd:match("[%s]" .. keyword .. "[%s$]") then - return true - end - end - return false -end - -local function prompt_user_tool_accept(tool_call, conf) - 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 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, ("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, ("Unknown tool type: '%s'"):format(call.tool or "nil")) - end - end - end - - 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 -return M diff --git a/lua/chatgpt_nvim/tools/read_file.lua b/lua/chatgpt_nvim/tools/read_file.lua deleted file mode 100644 index 5519b20..0000000 --- a/lua/chatgpt_nvim/tools/read_file.lua +++ /dev/null @@ -1,17 +0,0 @@ -local uv = vim.loop -local M = {} - -M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file) - local path = tool_call.path - if not path then - return "[read_file] Missing 'path'." - end - local file_data = read_file(path) - if file_data then - return string.format("Tool [read_file for '%s'] Result:\n\n%s", path, file_data) - else - return string.format("Tool [read_file for '%s'] FAILED. File not found or not readable.", path) - end -end - -return M diff --git a/lua/chatgpt_nvim/tools/replace_in_file.lua b/lua/chatgpt_nvim/tools/replace_in_file.lua deleted file mode 100644 index 976f360..0000000 --- a/lua/chatgpt_nvim/tools/replace_in_file.lua +++ /dev/null @@ -1,51 +0,0 @@ -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) - local updated = original - for _, r in ipairs(replacements) do - local search_str = r.search or "" - local replace_str = r.replace or "" - updated = updated:gsub(search_str, replace_str) - end - return updated -end - -M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file) - local path = tool_call.path - local replacements = tool_call.replacements or {} - - if not path or #replacements == 0 then - return "[replace_in_file] Missing 'path' or 'replacements'." - end - local root = vim.fn.getcwd() - - if not is_subpath(root, path) then - return string.format("Tool [replace_in_file for '%s'] REJECTED. Path outside project root.", path) - end - - local orig_data = read_file(path) - if not orig_data then - return string.format("[replace_in_file for '%s'] FAILED. Could not read file.", path) - end - - local updated_data = search_and_replace(orig_data, replacements) - handler.write_file(path, updated_data, conf) - - local msg = {} - msg[#msg+1] = string.format("[replace_in_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, 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 - -return M