Compare commits
6 Commits
59540981ed
...
e9fd204977
| Author | SHA1 | Date | |
|---|---|---|---|
| e9fd204977 | |||
| 7aa00813cf | |||
| 58da08e26f | |||
| 35bd0a7278 | |||
| 0ff77954db | |||
| fd8df2abd5 |
@@ -1,17 +1,27 @@
|
|||||||
project_name: "chatgpt_nvim"
|
project_name: "chatgpt_nvim"
|
||||||
default_prompt_blocks:
|
default_prompt_blocks:
|
||||||
- "basic-prompt"
|
- "basic-prompt"
|
||||||
- "workflow-prompt"
|
- "secure-coding"
|
||||||
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
|
||||||
token_limit: 128000
|
prompt_char_limit: 300000
|
||||||
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.
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -83,3 +83,18 @@ commands:
|
|||||||
The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
|
The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
|
||||||
|
|
||||||
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!
|
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!
|
||||||
|
|
||||||
|
## Test when developing
|
||||||
|
add new plugin to runtimepath
|
||||||
|
```vim
|
||||||
|
:set rtp^=~/temp_plugins/chatgpt.vim
|
||||||
|
```
|
||||||
|
Clear Lua module cache
|
||||||
|
```vim
|
||||||
|
:lua for k in pairs(package.loaded) do if k:match("^chatgpt_nvim") then package.loaded[k]=nil end end
|
||||||
|
```
|
||||||
|
Load new plugin code
|
||||||
|
```vim
|
||||||
|
:runtime plugin/chatgpt.vim
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ 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,
|
||||||
@@ -50,9 +49,17 @@ 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
|
||||||
@@ -98,12 +105,21 @@ 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
|
|
||||||
config.enable_debug_commands = result.enable_debug_commands
|
-- 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -111,7 +127,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 the default prompt blocks with the config's initial prompt
|
-- Merge default prompt blocks
|
||||||
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
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ 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
|
||||||
@@ -48,19 +54,6 @@ 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)
|
||||||
@@ -70,265 +63,98 @@ local function close_existing_buffer_by_name(pattern)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function preview_changes(changes, conf)
|
------------------------------------------------------------------------------
|
||||||
close_existing_buffer_by_name("ChatGPT_Changes_Preview$")
|
-- PROMPT CONSTRUCTION
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
------------------------------------------------------------------------------
|
||||||
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Changes_Preview")
|
local function build_tools_available_block()
|
||||||
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 "<no path>")
|
|
||||||
})
|
|
||||||
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
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.cmd("vsplit")
|
|
||||||
vim.cmd("buffer " .. bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
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 "<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 lines = {}
|
||||||
local line_num = 0
|
lines[#lines+1] = "<tools_available>"
|
||||||
for line in content:gmatch("([^\n]*)\n?") do
|
for _, t in ipairs(tools_module.available_tools) do
|
||||||
line_num = line_num + 1
|
lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s",
|
||||||
if line:find(search_string, 1, true) then
|
t.name, t.explanation, t.usage
|
||||||
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
|
||||||
|
lines[#lines+1] = "</tools_available>"
|
||||||
|
return table.concat(lines, "\n")
|
||||||
end
|
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) <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
|
||||||
|
if #file_content_blocks > 0 then
|
||||||
|
table.insert(final_sections, table.concat(file_content_blocks, "\n\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5) <environment_details>
|
||||||
|
local env_lines = {}
|
||||||
|
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
|
||||||
|
|
||||||
|
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 }
|
||||||
|
end
|
||||||
|
return { prompts["step-prompt"] }
|
||||||
|
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)
|
||||||
@@ -350,7 +176,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 prompt below.",
|
"# Enter your main user prompt (task) below.",
|
||||||
"",
|
"",
|
||||||
"Save & close with :wq, :x, or :bd to finalize your prompt."
|
"Save & close with :wq, :x, or :bd to finalize your prompt."
|
||||||
})
|
})
|
||||||
@@ -360,57 +186,25 @@ 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 prompt below.") then
|
if user_input == "" or user_input:find("^# Enter your main user prompt %(task%) 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 project_structure = context.get_project_structure(dirs, conf)
|
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||||
local initial_files = conf.initial_files or {}
|
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||||
local included_sections = {}
|
|
||||||
|
|
||||||
for _, item in ipairs(initial_files) do
|
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
|
||||||
local root = vim.fn.getcwd()
|
local bufnr_ref = vim.api.nvim_create_buf(false, true)
|
||||||
local full_path = root .. "/" .. item
|
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
|
||||||
if is_directory(full_path) then
|
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
|
||||||
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 = {
|
local prompt_lines = vim.split(chunks[1], "\n")
|
||||||
"## Basic Prompt Instructions:\n",
|
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
|
||||||
conf.initial_prompt .. "\n\n\n",
|
vim.cmd("vsplit")
|
||||||
"## User Instructions:\n",
|
vim.cmd("buffer " .. bufnr_ref)
|
||||||
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")
|
||||||
@@ -425,6 +219,9 @@ 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)
|
||||||
@@ -441,19 +238,22 @@ local function run_chatgpt_paste_command()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if data.commands and conf.enable_debug_commands then
|
-- Check if we have tools
|
||||||
local results = {}
|
if data.tools then
|
||||||
for _, cmd in ipairs(data.commands) do
|
-- Must also verify project name
|
||||||
table.insert(results, execute_debug_command(cmd, conf))
|
if not data.project_name or data.project_name ~= conf.project_name then
|
||||||
end
|
vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
|
||||||
local output = table.concat(results, "\n\n")
|
|
||||||
copy_to_clipboard(output)
|
|
||||||
print("Debug command results copied to clipboard!")
|
|
||||||
return
|
return
|
||||||
end
|
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.")
|
||||||
|
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
|
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
|
||||||
@@ -469,7 +269,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
|
||||||
preview_changes(data.files, conf)
|
require('chatgpt_nvim.init').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()
|
||||||
@@ -489,7 +289,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 = partial_accept(data.files, conf)
|
final_files = require('chatgpt_nvim.init').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
|
||||||
@@ -500,14 +300,10 @@ 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'.")
|
||||||
goto continue
|
else
|
||||||
end
|
|
||||||
|
|
||||||
if not is_subpath(root, fileinfo.path) then
|
if not is_subpath(root, fileinfo.path) then
|
||||||
vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path)
|
vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path)
|
||||||
goto continue
|
else
|
||||||
end
|
|
||||||
|
|
||||||
if fileinfo.delete == true then
|
if fileinfo.delete == true then
|
||||||
ui.debug_log("Deleting file: " .. fileinfo.path)
|
ui.debug_log("Deleting file: " .. fileinfo.path)
|
||||||
handler.delete_file(fileinfo.path, conf)
|
handler.delete_file(fileinfo.path, conf)
|
||||||
@@ -519,11 +315,13 @@ local function run_chatgpt_paste_command()
|
|||||||
else
|
else
|
||||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.")
|
vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.")
|
||||||
end
|
end
|
||||||
::continue::
|
|
||||||
end
|
end
|
||||||
|
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)
|
||||||
@@ -531,14 +329,13 @@ 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: `" .. f .. "`\n```\n" .. content .. "\n```\n")
|
table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content))
|
||||||
else
|
else
|
||||||
table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n(Could not read file)\n```\n")
|
table.insert(file_sections, ("\nFile: `%s`\n```\n(Could not read file)\n```\n"):format(f))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -547,7 +344,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. If you have all information you need, provide the final YAML with `project_name` and `files` (with `content` or `delete`) to apply changes.\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."
|
||||||
}
|
}
|
||||||
|
|
||||||
local prompt = table.concat(sections, "\n")
|
local prompt = table.concat(sections, "\n")
|
||||||
@@ -565,17 +362,21 @@ 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 the ChatGPT O1 model.")
|
print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
vim.api.nvim_err_writeln("Invalid response. Expected 'project_name' and 'files'.")
|
vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & 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 {"."}
|
||||||
@@ -586,92 +387,19 @@ local function run_chatgpt_current_buffer_command()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local project_structure = context.get_project_structure(dirs, conf)
|
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||||
local initial_files = conf.initial_files or {}
|
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||||
local included_sections = {}
|
|
||||||
|
|
||||||
local function is_directory(path)
|
|
||||||
local stat = vim.loop.fs_stat(path)
|
|
||||||
return stat and stat.type == "directory"
|
|
||||||
end
|
|
||||||
|
|
||||||
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$")
|
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
|
||||||
local bufnr_ref = vim.api.nvim_create_buf(false, true)
|
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_name(bufnr_ref, "ChatGPT_Generated_Prompt")
|
||||||
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
|
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
|
||||||
|
|
||||||
local lines_ref = {
|
local prompt_lines = vim.split(chunks[1], "\n")
|
||||||
"# Below is the generated prompt. You can keep it for reference:",
|
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
|
||||||
""
|
|
||||||
}
|
|
||||||
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("vsplit")
|
||||||
vim.cmd("buffer " .. bufnr_ref)
|
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")
|
||||||
@@ -680,9 +408,11 @@ 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
|
||||||
|
|||||||
@@ -256,54 +256,41 @@ local M = {
|
|||||||
["basic-prompt"] = [[
|
["basic-prompt"] = [[
|
||||||
### Basic Prompt
|
### Basic Prompt
|
||||||
|
|
||||||
1. **Analyze Required Files**
|
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:
|
||||||
- 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:
|
1. `project_name: "<actual_project_name>"` (matching the real project name)
|
||||||
|
2. A `tools:` array describing the operations you want to perform.
|
||||||
|
|
||||||
|
**Example** (substitute `<actual_project_name>` with the real name):
|
||||||
```yaml
|
```yaml
|
||||||
project_name: my_project
|
project_name: "my_project"
|
||||||
files:
|
tools:
|
||||||
- path: "relative/path/to/needed_file"
|
- tool: "readFile"
|
||||||
```
|
path: "relative/path/to/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.
|
|
||||||
|
|
||||||
2. **Request File Contents**
|
- tool: "replace_in_file"
|
||||||
- For every file you need but don’t have, provide a short YAML snippet (like above) to retrieve it.
|
path: "relative/path/to/file"
|
||||||
- Ask any clarifying questions outside the YAML code block.
|
replacements:
|
||||||
|
- search: "old text"
|
||||||
|
replace: "new text"
|
||||||
|
|
||||||
3. **Provide Output YAML**
|
- tool: "editFile"
|
||||||
- When you have all information, output the final YAML in this format:
|
path: "relative/path/to/file"
|
||||||
```yaml
|
|
||||||
project_name: my_project
|
|
||||||
|
|
||||||
files:
|
|
||||||
- path: "src/main.py"
|
|
||||||
content: |
|
content: |
|
||||||
# Updated main function
|
# Full updated file content here
|
||||||
def main():
|
|
||||||
print("Hello, World!")
|
|
||||||
|
|
||||||
- path: "docs/README.md"
|
- tool: "executeCommand"
|
||||||
delete: true
|
command: "ls -la"
|
||||||
```
|
```
|
||||||
- `project_name` must match the project’s configured name.
|
|
||||||
- Each file item in `files` must have `path` and either `content` or `delete: true`.
|
|
||||||
|
|
||||||
4. **Explain Changes**
|
**Key Points**:
|
||||||
- After the final YAML, add a brief explanation of the modifications (outside the YAML).
|
- Always include `project_name: "<actual_project_name>"` in the same YAML as `tools`.
|
||||||
|
- If you only need one tool, include just one object in the `tools` array.
|
||||||
5. **Iterate as Needed**
|
- If multiple tools are needed, list them sequentially in the `tools` array.
|
||||||
- If further context or changes are required, repeat steps to request files or clarifications.
|
- 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.
|
||||||
6. **Important Notes**
|
- The plugin will verify the `project_name` is correct before running any tools.
|
||||||
- Never modify or delete a file you haven’t explicitly requested or received.
|
- If the response grows too large, I'll guide you to break it into smaller steps.
|
||||||
- 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"] = [[
|
||||||
### Secure Coding Guidelines
|
### Secure Coding Guidelines
|
||||||
@@ -370,33 +357,10 @@ 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.
|
|
||||||
]]
|
]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
lua/chatgpt_nvim/tools/edit_file.lua
Normal file
37
lua/chatgpt_nvim/tools/edit_file.lua
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
17
lua/chatgpt_nvim/tools/execute_command.lua
Normal file
17
lua/chatgpt_nvim/tools/execute_command.lua
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
39
lua/chatgpt_nvim/tools/init.lua
Normal file
39
lua/chatgpt_nvim/tools/init.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
177
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
177
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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
|
||||||
79
lua/chatgpt_nvim/tools/manager.lua
Normal file
79
lua/chatgpt_nvim/tools/manager.lua
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
17
lua/chatgpt_nvim/tools/read_file.lua
Normal file
17
lua/chatgpt_nvim/tools/read_file.lua
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
51
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal file
51
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
Reference in New Issue
Block a user