Compare commits

..

No commits in common. "e9fd204977b975110b18635802f68d8165b2b2f1" and "59540981edeebd7faf9894e2ba40cbe4fb02f31c" have entirely different histories.

12 changed files with 516 additions and 668 deletions

View file

@ -1,27 +1,17 @@
project_name: "chatgpt_nvim" project_name: "chatgpt_nvim"
default_prompt_blocks: default_prompt_blocks:
- "basic-prompt" - "basic-prompt"
- "secure-coding" - "workflow-prompt"
directories:
- "."
initial_files: initial_files:
- "README.md" - "README.md"
debug: false debug: false
improved_debug: false
preview_changes: false preview_changes: false
interactive_file_selection: false interactive_file_selection: false
partial_acceptance: false partial_acceptance: false
improved_debug: false
enable_debug_commands: true enable_debug_commands: true
prompt_char_limit: 300000 token_limit: 128000
enable_chunking: false enable_chunking: false
enable_step_by_step: true 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.

View file

@ -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. The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support! Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!
## Test when developing
add new plugin to runtimepath
```vim
:set rtp^=~/temp_plugins/chatgpt.vim
```
Clear Lua module cache
```vim
:lua for k in pairs(package.loaded) do if k:match("^chatgpt_nvim") then package.loaded[k]=nil end end
```
Load new plugin code
```vim
:runtime plugin/chatgpt.vim
```

View file

@ -40,6 +40,7 @@ function M.load()
initial_prompt = "", initial_prompt = "",
directories = { "." }, directories = { "." },
default_prompt_blocks = {}, default_prompt_blocks = {},
-- Changed default from 128000 to 16384 as requested
prompt_char_limit = 300000, prompt_char_limit = 300000,
project_name = "", project_name = "",
debug = false, debug = false,
@ -49,17 +50,9 @@ function M.load()
partial_acceptance = false, partial_acceptance = false,
improved_debug = false, improved_debug = false,
enable_chunking = false, enable_chunking = false,
-- New default for step-by-step
enable_step_by_step = true, enable_step_by_step = true,
enable_debug_commands = false
-- 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,
}
} }
if fd then if fd then
@ -105,21 +98,12 @@ function M.load()
if type(result.enable_chunking) == "boolean" then if type(result.enable_chunking) == "boolean" then
config.enable_chunking = result.enable_chunking config.enable_chunking = result.enable_chunking
end end
-- Added logic to load enable_step_by_step from user config
if type(result.enable_step_by_step) == "boolean" then if type(result.enable_step_by_step) == "boolean" then
config.enable_step_by_step = result.enable_step_by_step config.enable_step_by_step = result.enable_step_by_step
end end
if type(result.enable_debug_commands) == "boolean" then
-- auto_lint controls whether we do LSP-based checks config.enable_debug_commands = result.enable_debug_commands
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
end end
end 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..." config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
end 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 if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
local merged_prompt = {} local merged_prompt = {}
for _, block_name in ipairs(config.default_prompt_blocks) do for _, block_name in ipairs(config.default_prompt_blocks) do

View file

@ -6,14 +6,8 @@ local config = require('chatgpt_nvim.config')
local ui = require('chatgpt_nvim.ui') local ui = require('chatgpt_nvim.ui')
local prompts = require('chatgpt_nvim.prompts') 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") local ok_yaml, lyaml = pcall(require, "lyaml")
------------------------------------------------------------------------------
-- UTILITIES
------------------------------------------------------------------------------
local function copy_to_clipboard(text) local function copy_to_clipboard(text)
vim.fn.setreg('+', text) vim.fn.setreg('+', text)
end end
@ -54,6 +48,19 @@ local function read_file(path)
return data return data
end 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) local function close_existing_buffer_by_name(pattern)
for _, b in ipairs(vim.api.nvim_list_bufs()) do for _, b in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(b) local name = vim.api.nvim_buf_get_name(b)
@ -63,98 +70,265 @@ local function close_existing_buffer_by_name(pattern)
end end
end end
------------------------------------------------------------------------------ local function preview_changes(changes, conf)
-- PROMPT CONSTRUCTION close_existing_buffer_by_name("ChatGPT_Changes_Preview$")
------------------------------------------------------------------------------ local bufnr = vim.api.nvim_create_buf(false, true)
local function build_tools_available_block() vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Changes_Preview")
local lines = {} vim.api.nvim_buf_set_option(bufnr, "filetype", "diff")
lines[#lines+1] = "<tools_available>" vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
for _, t in ipairs(tools_module.available_tools) do "# Preview of Changes:",
lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s", "# (Close this window to apply changes or use :q to cancel)",
t.name, t.explanation, t.usage ""
) })
end for _, fileinfo in ipairs(changes) do
lines[#lines+1] = "</tools_available>" local indicator = (fileinfo.delete == true) and "Delete file" or "Write file"
return table.concat(lines, "\n") vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
end string.format("=== %s: %s ===", indicator, fileinfo.path or "<no path>")
})
local function build_prompt(user_input, dirs, conf) if fileinfo.content then
local root = vim.fn.getcwd() local lines = vim.split(fileinfo.content, "\n")
local initial_files = conf.initial_files or {} for _, line in ipairs(lines) do
local final_sections = {} vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line })
-- 1) <initial_prompts>
if conf.initial_prompt and conf.initial_prompt ~= "" then
table.insert(final_sections, "<initial_prompts>\n" .. conf.initial_prompt .. "\n</initial_prompts>\n")
end
-- 2) <tools_available>
table.insert(final_sections, build_tools_available_block())
-- 3) <task>
local task_lines = {}
task_lines[#task_lines+1] = "<task>"
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] = "</task>\n"
table.insert(final_sections, table.concat(task_lines, "\n"))
-- 4) <file_content path="...">
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(
"<file_content path=\"%s\">\n%s\n</file_content>", file_path, fdata
)
end end
end end
end vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
if #file_content_blocks > 0 then
table.insert(final_sections, table.concat(file_content_blocks, "\n\n"))
end end
-- 5) <environment_details> vim.cmd("vsplit")
local env_lines = {} vim.cmd("buffer " .. bufnr)
env_lines[#env_lines+1] = "<environment_details>"
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] = "</environment_details>"
table.insert(final_sections, table.concat(env_lines, "\n"))
return table.concat(final_sections, "\n\n")
end end
local function handle_step_by_step_if_needed(prompt, conf) local function partial_accept(changes, conf)
local length = #prompt close_existing_buffer_by_name("ChatGPT_Partial_Accept$")
local limit = conf.prompt_char_limit or 8000 local bufnr = vim.api.nvim_create_buf(false, true)
if (not conf.enable_step_by_step) or (length <= limit) then vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Partial_Accept")
return { prompt } 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 "<no path>"))
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='<text>', target='<path>'}"
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 end
return { prompts["step-prompt"] }
end end
------------------------------------------------------------------------------
-- :ChatGPT
------------------------------------------------------------------------------
local function run_chatgpt_command() local function run_chatgpt_command()
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) 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_option(bufnr, "modifiable", true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 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." "Save & close with :wq, :x, or :bd to finalize your prompt."
}) })
@ -186,25 +360,57 @@ local function run_chatgpt_command()
callback = function() callback = function()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local user_input = table.concat(lines, "\n") 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_out_write("No valid input provided.\n")
vim.api.nvim_buf_set_option(bufnr, "modified", false) vim.api.nvim_buf_set_option(bufnr, "modified", false)
return return
end end
local final_prompt = build_prompt(user_input, dirs, conf) local project_structure = context.get_project_structure(dirs, conf)
local chunks = handle_step_by_step_if_needed(final_prompt, conf) local initial_files = conf.initial_files or {}
local included_sections = {}
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") for _, item in ipairs(initial_files) do
local bufnr_ref = vim.api.nvim_create_buf(false, true) local root = vim.fn.getcwd()
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") local full_path = root .. "/" .. item
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") 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") local initial_sections = {
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) "## Basic Prompt Instructions:\n",
vim.cmd("vsplit") conf.initial_prompt .. "\n\n\n",
vim.cmd("buffer " .. bufnr_ref) "## 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]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n") 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) vim.cmd("buffer " .. bufnr)
end end
------------------------------------------------------------------------------
-- :ChatGPTPaste
------------------------------------------------------------------------------
local function run_chatgpt_paste_command() local function run_chatgpt_paste_command()
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) ui.setup_ui(conf)
@ -238,22 +441,19 @@ local function run_chatgpt_paste_command()
return return
end end
-- Check if we have tools if data.commands and conf.enable_debug_commands then
if data.tools then local results = {}
-- Must also verify project name for _, cmd in ipairs(data.commands) do
if not data.project_name or data.project_name ~= conf.project_name then table.insert(results, execute_debug_command(cmd, conf))
vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
return
end end
local output = table.concat(results, "\n\n")
local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file) copy_to_clipboard(output)
copy_to_clipboard(output_messages) print("Debug command results copied to clipboard!")
print("Tool call results have been processed and copied to clipboard.")
return return
end 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 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 if data.project_name ~= conf.project_name then
vim.api.nvim_err_writeln("Project name mismatch. Aborting.") vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
return return
@ -269,7 +469,7 @@ local function run_chatgpt_paste_command()
if is_final then if is_final then
if conf.preview_changes 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.") print("Close the preview window to apply changes, or use :q to cancel.")
local closed = vim.wait(60000, function() local closed = vim.wait(60000, function()
local bufs = vim.api.nvim_list_bufs() local bufs = vim.api.nvim_list_bufs()
@ -289,7 +489,7 @@ local function run_chatgpt_paste_command()
local final_files = data.files local final_files = data.files
if conf.partial_acceptance then 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 if #final_files == 0 then
vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.") vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.")
return return
@ -300,28 +500,30 @@ local function run_chatgpt_paste_command()
for _, fileinfo in ipairs(final_files) do for _, fileinfo in ipairs(final_files) do
if not fileinfo.path then if not fileinfo.path then
vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.") vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.")
else goto continue
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
end 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 end
else else
-- Not final => user is requesting more files
local requested_paths = {} local requested_paths = {}
local root = vim.fn.getcwd()
for _, fileinfo in ipairs(data.files) do for _, fileinfo in ipairs(data.files) do
if fileinfo.path then if fileinfo.path then
table.insert(requested_paths, fileinfo.path) table.insert(requested_paths, fileinfo.path)
@ -329,13 +531,14 @@ local function run_chatgpt_paste_command()
end end
local file_sections = {} local file_sections = {}
local root = vim.fn.getcwd()
for _, f in ipairs(requested_paths) do for _, f in ipairs(requested_paths) do
local path = root .. "/" .. f local path = root .. "/" .. f
local content = read_file(path) local content = read_file(path)
if content then 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 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
end end
@ -344,7 +547,7 @@ local function run_chatgpt_paste_command()
"\n\nProject name: " .. (conf.project_name or ""), "\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", "\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"), 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") local prompt = table.concat(sections, "\n")
@ -362,21 +565,17 @@ local function run_chatgpt_paste_command()
end end
copy_to_clipboard(prompt) 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 end
else 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
end end
------------------------------------------------------------------------------
-- :ChatGPTCurrentBuffer
------------------------------------------------------------------------------
local function run_chatgpt_current_buffer_command() local function run_chatgpt_current_buffer_command()
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) ui.setup_ui(conf)
ui.debug_log("Running :ChatGPTCurrentBuffer command.") ui.debug_log("Running :ChatGPTCurrentBuffer command.")
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local user_input = table.concat(lines, "\n") local user_input = table.concat(lines, "\n")
local dirs = conf.directories or {"."} local dirs = conf.directories or {"."}
@ -387,19 +586,92 @@ local function run_chatgpt_current_buffer_command()
end end
end end
local final_prompt = build_prompt(user_input, dirs, conf) local project_structure = context.get_project_structure(dirs, conf)
local chunks = handle_step_by_step_if_needed(final_prompt, conf) local initial_files = conf.initial_files or {}
local included_sections = {}
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$") local function is_directory(path)
local bufnr_ref = vim.api.nvim_create_buf(false, true) local stat = vim.loop.fs_stat(path)
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt") return stat and stat.type == "directory"
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown") end
local prompt_lines = vim.split(chunks[1], "\n") local function read_file(path)
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines) local fd = vim.loop.fs_open(path, "r", 438)
vim.cmd("vsplit") if not fd then
vim.cmd("buffer " .. bufnr_ref) 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]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n") 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
end end
------------------------------------------------------------------------------
-- PUBLIC API
------------------------------------------------------------------------------
M.run_chatgpt_command = run_chatgpt_command M.run_chatgpt_command = run_chatgpt_command
M.run_chatgpt_paste_command = run_chatgpt_paste_command M.run_chatgpt_paste_command = run_chatgpt_paste_command
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
M.execute_debug_command = execute_debug_command
return M return M

View file

@ -44,7 +44,7 @@ local M = {
5. **Styling & CSS Management** 5. **Styling & CSS Management**
- Use your preferred styling approach (CSS Modules, [Tailwind CSS](https://tailwindcss.com/), or standard CSS/SCSS files). - 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. - 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 Solids reactivity. - If using CSS-in-JS solutions or third-party libraries, ensure they integrate cleanly with Solids reactivity.
6. **TypeScript & Linting** 6. **TypeScript & Linting**
- Use **TypeScript** to ensure type safety and improve maintainability. - Use **TypeScript** to ensure type safety and improve maintainability.
@ -81,7 +81,7 @@ local M = {
1. **Go Modules** 1. **Go Modules**
- Use a single `go.mod` file at the project root for module management. - Use a single `go.mod` file at the project root for module management.
- Ensure you use proper import paths based on the module name. - Ensure you use proper import paths based on the module name.
- If you refer to internal packages, use relative paths consistent with the modules structure (e.g., `moduleName/internal/packageA`). - If you refer to internal packages, use relative paths consistent with the modules structure (e.g., `moduleName/internal/packageA`).
2. **Package Structure** 2. **Package Structure**
- Each folder should contain exactly one package. - Each folder should contain exactly one package.
@ -110,21 +110,21 @@ local M = {
4. **Coding Best Practices** 4. **Coding Best Practices**
- Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols). - Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols).
- Keep functions short, focused, and tested. - Keep functions short, focused, and tested.
- Use Gos standard library where possible before adding third-party dependencies. - Use Gos standard library where possible before adding third-party dependencies.
- When introducing new functions or types, ensure they are uniquely named to avoid collisions. - When introducing new functions or types, ensure they are uniquely named to avoid collisions.
5. **Import Management** 5. **Import Management**
- Ensure that every import is actually used in your code. - Ensure that every import is actually used in your code.
- Remove unused imports to keep your code clean and maintainable. - Remove unused imports to keep your code clean and maintainable.
- Include all necessary imports for anything referenced in your code to avoid missing imports. - Include all necessary imports for anything referenced in your code to avoid missing imports.
- Verify that any introduced import paths match your modules structure and do not cause naming conflicts. - Verify that any introduced import paths match your modules structure and do not cause naming conflicts.
6. **Output Format** 6. **Output Format**
- Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule. - 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.). - 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. - 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 Golangs best practices for large, modular projects. Please follow these guidelines to ensure the generated or explained code aligns well with Golangs best practices for large, modular projects.
]], ]],
["typo3-development"] = [[ ["typo3-development"] = [[
### TYPO3 Development Guidelines ### TYPO3 Development Guidelines
@ -150,10 +150,10 @@ local M = {
- Keep site configuration in `config/sites/` (for TYPO3 v9+). - Keep site configuration in `config/sites/` (for TYPO3 v9+).
2. **Extension Development** 2. **Extension Development**
- Create custom functionality as separate extensions (site packages, domain-specific extensions, etc.) following TYPO3s recommended structure: - Create custom functionality as separate extensions (site packages, domain-specific extensions, etc.) following TYPO3s recommended structure:
- **Key files**: `ext_emconf.php`, `ext_localconf.php`, `ext_tables.php`, `ext_tables.sql`, `Configuration/`, `Classes/`, `Resources/`. - **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`). - Use **PSR-4** autoloading and name extensions logically (e.g., `my_sitepackage`, `my_blogextension`).
- Keep your extensions code under `Classes/` (e.g., Controllers, Models, Services). - Keep your extensions 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`). - Place Fluid templates, partials, and layouts under `Resources/Private/` (e.g., `Resources/Private/Templates`, `Resources/Private/Partials`, `Resources/Private/Layouts`).
3. **Configuration (TypoScript & TCA)** 3. **Configuration (TypoScript & TCA)**
@ -186,10 +186,10 @@ local M = {
7. **Output Format** 7. **Output Format**
- Present any generated source code or configuration files in a well-organized structure. - 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. - 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 sites 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 sites main configuration.).
- If you modify or extend an existing file, specify precisely which changes or additions you are making. - 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 TYPO3s best practices for large, maintainable projects. Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3s best practices for large, maintainable projects.
]], ]],
["rust-development"] = [[ ["rust-development"] = [[
### Rust Development Guidelines ### Rust Development Guidelines
@ -204,11 +204,11 @@ local M = {
2. **Crates & Packages** 2. **Crates & Packages**
- Split the application into logical crates (libraries and/or binaries). - 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. - 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 Rusts naming conventions (e.g., `snake_case` for files/modules, `PascalCase` for types). - Name crates, modules, and files clearly, following Rusts 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. - Avoid duplicating the same function or type in multiple crates; share common functionality via a dedicated library crate if needed.
3. **Folder & Module Structure** 3. **Folder & Module Structure**
- Organize code within each crate using Rusts module system, keeping related functions and types in logical modules/submodules. - Organize code within each crate using Rusts module system, keeping related functions and types in logical modules/submodules.
- A typical directory layout for a workspace with multiple crates might look like: - A typical directory layout for a workspace with multiple crates might look like:
``` ```
myproject/ myproject/
@ -226,7 +226,7 @@ local M = {
target/ target/
... ...
``` ```
- If you have integration tests, store them in a `tests/` folder at the crate root, or use the workspace roots `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 roots `tests/` directory if they span multiple crates.
4. **Coding & Documentation Best Practices** 4. **Coding & Documentation Best Practices**
- Write **idiomatic Rust** code: - Write **idiomatic Rust** code:
@ -256,41 +256,54 @@ local M = {
["basic-prompt"] = [[ ["basic-prompt"] = [[
### 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 files 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: "<actual_project_name>"` (matching the real project name) 2. **Request File Contents**
2. A `tools:` array describing the operations you want to perform. - For every file you need but dont have, provide a short YAML snippet (like above) to retrieve it.
- Ask any clarifying questions outside the YAML code block.
**Example** (substitute `<actual_project_name>` with the real name): 3. **Provide Output YAML**
```yaml - When you have all information, output the final YAML in this format:
project_name: "my_project" ```yaml
tools: project_name: my_project
- tool: "readFile"
path: "relative/path/to/file"
- tool: "replace_in_file" files:
path: "relative/path/to/file" - path: "src/main.py"
replacements: content: |
- search: "old text" # Updated main function
replace: "new text" def main():
print("Hello, World!")
- tool: "editFile" - path: "docs/README.md"
path: "relative/path/to/file" delete: true
content: | ```
# Full updated file content here - `project_name` must match the projects configured name.
- Each file item in `files` must have `path` and either `content` or `delete: true`.
- tool: "executeCommand" 4. **Explain Changes**
command: "ls -la" - After the final YAML, add a brief explanation of the modifications (outside the YAML).
```
**Key Points**: 5. **Iterate as Needed**
- Always include `project_name: "<actual_project_name>"` in the same YAML as `tools`. - If further context or changes are required, repeat steps to request files or clarifications.
- 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. 6. **Important Notes**
- Always run at least one tool (e.g., `readFile`, `editFile`, `executeCommand`), exept you have finished. - Never modify or delete a file you havent explicitly requested or received.
- Always just include one yaml in the response with all the tools you want to run in that yaml. - Use comments in code only to clarify code logic, not to explain your thought process.
- The plugin will verify the `project_name` is correct before running any tools. - Explanations go outside the YAML.
- If the response grows too large, I'll guide you to break it into smaller steps.
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"] = [[
### Secure Coding Guidelines ### Secure Coding Guidelines
@ -357,10 +370,33 @@ local M = {
["step-prompt"] = [[ ["step-prompt"] = [[
It appears this request might exceed the model's prompt character limit if done all at once. 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. 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! Thank you!
]], ]],
["file-selection-instructions"] = [[ ["file-selection-instructions"] = [[
Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd) 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.
]] ]]
} }

View file

@ -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("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content)
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
-- 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data)
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
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