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