change-to-tools #2

Merged
dominik.polakovics merged 5 commits from change-to-tools into main 2025-01-31 13:38:30 +01:00
12 changed files with 658 additions and 506 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 files 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 dont 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 projects 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 havent 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.
]] ]]
} }

View 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

View 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

View 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

View 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

View 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

View 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

View 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