Compare commits

...

17 Commits

Author SHA1 Message Date
7aa00813cf feat: change the lsp buffer naming and improve prompt 2025-01-31 13:38:08 +01:00
58da08e26f feat: remove lint tool and add lsp integration 2025-01-31 11:13:58 +01:00
35bd0a7278 feat: add lint tool 2025-01-31 10:43:28 +01:00
0ff77954db feat: remove unneeded debug command approach 2025-01-31 10:31:09 +01:00
fd8df2abd5 initial change to tools logic 2025-01-31 09:49:15 +01:00
59540981ed feat: change prompt 2025-01-24 02:47:27 +01:00
e36ad99dcb update prompts 2025-01-24 01:51:06 +01:00
f7d986f5fc feat: add solidjs prompt 2025-01-19 13:42:11 +01:00
b7274a310b feat: change rust prompt, change go prompt for better import matching 2025-01-19 13:23:00 +01:00
904979b0f6 feat: improve prompts 2025-01-19 00:58:15 +01:00
b9643952cb feat: dont show files and folders in list which are in .gitignore 2025-01-07 17:58:57 +01:00
1520641a04 feat: add symlink detection 2025-01-07 13:02:43 +01:00
f1cc371294 feat: change commands to accept args 2025-01-05 17:11:13 +01:00
ca729140c3 fix: load config just once per call 2025-01-05 01:45:43 +01:00
6950c542ad fix: just load conf once per command 2025-01-04 21:37:48 +01:00
3505a295a9 feat: move prompts to a separat file 2025-01-04 21:14:12 +01:00
2c855e881b feat: change to prompt_char_limit instead of token limit 2025-01-04 20:18:44 +01:00
15 changed files with 1108 additions and 560 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

@@ -2,70 +2,7 @@ local M = {}
local uv = vim.loop local uv = vim.loop
local ok_yaml, lyaml = pcall(require, "lyaml") local ok_yaml, lyaml = pcall(require, "lyaml")
local prompts = require("chatgpt_nvim.prompts")
local prompt_blocks = {
["go-development"] = [[
You are a coding assistant specialized in Go development.
You will receive a projects context and user instructions related to Go code,
and you must return the requested modifications or guidance.
When returning modifications, follow the specified YAML structure.
Keep your suggestions aligned with Go best practices and idiomatic Go.
]],
["typo3-development"] = [[
You are a coding assistant specialized in TYPO3 development.
You have access to the projects context and the users instructions.
Your answers should focus on TYPO3 coding guidelines, extension development best practices,
and TSconfig or TypoScript recommendations.
]],
["rust-development"] = [[
You are a coding assistant specialized in Rust development.
You will receive a projects context and user instructions related to Rust code,
and you must return the requested modifications or guidance.
When returning modifications, follow the specified YAML structure.
Keep your suggestions aligned with Rust best practices and idiomatic Rust.
]],
["basic-prompt"] = [[
You are a coding assistant who receives a project's context and user instructions.
The user will provide a prompt, and you will guide them through a workflow:
1. First, you should analyse which files you need to solve the request.
You can see which files are present in the provided project structure.
Additionally if presented you could also ask for files of a library which is provided in for example composer.json.
2. If file contents is needed provide a yaml which asks for the file contents.
For example:
project_name: example_project
files:
- path: "relative/path/to/file"
3. If more information or context is needed, ask the user (outside of the YAML) to provide that.
4. When all necessary information is gathered, provide the final YAML with the
project's name and a list of files to be created or modified.
Also explain the changes you made below the yaml.
The final YAML must have a top-level key named 'project_name' that matches the project's configured name,
and a top-level key named 'files', which is a list of file changes. Each element in 'files' must be a mapping with:
- 'path' for the file path relative to the projects root directory.
- either 'content' with a multiline string for new content, or 'delete: true' if the file should be deleted.
Important: dont use comments in the code to explain which steps you have taken.
Comments should just explain the code and not your thought process.
You can explain your thought process outside of the YAML.
If more context is needed at any point before providing the final YAML, request it outside of the YAML.
Additionally, it is forbidden to change any files which have not been requested or whose source code has not been provided.
]],
["secure-coding"] = [[
You are a coding assistant specialized in secure software development.
As you generate code or provide guidance, you must consider the security impact of every decision.
You will write and review code with a focus on minimizing vulnerabilities and following best security practices,
such as validating all user inputs, avoiding unsafe libraries or functions, and following secure coding standards.
]],
["workflow-prompt"] = [[
You are a coding assistant focusing on making the Neovim ChatGPT workflow straightforward and user-friendly.
Provide a concise set of steps or guidance, reminding the user:
- How to list needed files for further context
- How to request additional information outside of the YAML
- How to finalize changes with a YAML response containing project_name and files
Always ensure that prompts and explanations remain clear and minimal, reducing user errors.
]]
}
local function get_project_root() local function get_project_root()
local current_file = vim.fn.expand("%:p") local current_file = vim.fn.expand("%:p")
@@ -103,8 +40,7 @@ function M.load()
initial_prompt = "", initial_prompt = "",
directories = { "." }, directories = { "." },
default_prompt_blocks = {}, default_prompt_blocks = {},
-- Changed default from 128000 to 16384 as requested prompt_char_limit = 300000,
token_limit = 16384,
project_name = "", project_name = "",
debug = false, debug = false,
initial_files = {}, initial_files = {},
@@ -113,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
@@ -134,8 +78,8 @@ function M.load()
if type(result.default_prompt_blocks) == "table" then if type(result.default_prompt_blocks) == "table" then
config.default_prompt_blocks = result.default_prompt_blocks config.default_prompt_blocks = result.default_prompt_blocks
end end
if type(result.token_limit) == "number" then if type(result.prompt_char_limit) == "number" then
config.token_limit = result.token_limit config.prompt_char_limit = result.prompt_char_limit
end end
if type(result.project_name) == "string" then if type(result.project_name) == "string" then
config.project_name = result.project_name config.project_name = result.project_name
@@ -161,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
@@ -174,12 +127,12 @@ 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
if prompt_blocks[block_name] then if prompts[block_name] then
table.insert(merged_prompt, prompt_blocks[block_name]) table.insert(merged_prompt, prompts[block_name])
end end
end end
if #merged_prompt > 0 then if #merged_prompt > 0 then

View File

@@ -1,10 +1,39 @@
local M = {} local M = {}
local uv = vim.loop local uv = vim.loop
local config = require('chatgpt_nvim.config')
local function load_gitignore_patterns(root) -- Converts a .gitignore-style pattern to a Lua pattern
local conf = config.load() local function gitignore_to_lua_pattern(gip)
-- Trim spaces
gip = gip:gsub("^%s*(.-)%s*$", "%1")
-- Escape magic chars in Lua patterns
local magic_chars = "().^$+%-*?[]"
gip = gip:gsub("["..magic_chars.."]", "%%%1")
-- Convert ** to .- (match any path, including dirs)
gip = gip:gsub("%%%%%*%%%%%*", ".*")
-- Convert * to [^/]* (match anything except /)
gip = gip:gsub("%%%%%*", "[^/]*")
-- If pattern starts with /, ensure it matches start of string
if gip:sub(1,1) == "/" then
gip = "^" .. gip:sub(2)
else
-- Otherwise allow matching anywhere
gip = gip
end
-- If pattern ends with /, ensure it matches a directory
if gip:sub(-1) == "/" then
gip = gip .. ".*"
end
return gip
end
local function load_gitignore_patterns(root, conf)
local gitignore_path = root .. "/.gitignore" local gitignore_path = root .. "/.gitignore"
local fd = uv.fs_open(gitignore_path, "r", 438) local fd = uv.fs_open(gitignore_path, "r", 438)
if not fd then if not fd then
@@ -19,9 +48,9 @@ local function load_gitignore_patterns(root)
if not data then return {} end if not data then return {} end
local patterns = {} local patterns = {}
for line in data:gmatch("[^\r\n]+") do for line in data:gmatch("[^\r\n]+") do
line = line:match("^%s*(.-)%s*$") -- trim line = line:match("^%s*(.-)%s*$")
if line ~= "" and not line:match("^#") then if line ~= "" and not line:match("^#") then
patterns[#patterns+1] = line table.insert(patterns, gitignore_to_lua_pattern(line))
end end
end end
if conf.debug then if conf.debug then
@@ -30,10 +59,9 @@ local function load_gitignore_patterns(root)
return patterns return patterns
end end
local function should_ignore_file(file, ignore_patterns) local function should_ignore_file(file, ignore_patterns, conf)
local conf = config.load()
for _, pattern in ipairs(ignore_patterns) do for _, pattern in ipairs(ignore_patterns) do
if file:find(pattern, 1, true) then if file:match(pattern) then
if conf.debug then if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] Ignoring file/dir: " .. file .. " (matched pattern: " .. pattern .. ")\n") vim.api.nvim_out_write("[chatgpt_nvim:context] Ignoring file/dir: " .. file .. " (matched pattern: " .. pattern .. ")\n")
end end
@@ -43,8 +71,7 @@ local function should_ignore_file(file, ignore_patterns)
return false return false
end end
local function is_text_file(file) local function is_text_file(file, conf)
local conf = config.load()
local fd = uv.fs_open(file, "r", 438) local fd = uv.fs_open(file, "r", 438)
if not fd then if not fd then
if conf.debug then if conf.debug then
@@ -54,7 +81,6 @@ local function is_text_file(file)
end end
local chunk = uv.fs_read(fd, 1024, 0) or "" local chunk = uv.fs_read(fd, 1024, 0) or ""
uv.fs_close(fd) uv.fs_close(fd)
-- Check for null bytes as a heuristic for binary files
if chunk:find("\0") then if chunk:find("\0") then
if conf.debug then if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:context] File appears binary: " .. file .. "\n") vim.api.nvim_out_write("[chatgpt_nvim:context] File appears binary: " .. file .. "\n")
@@ -64,8 +90,7 @@ local function is_text_file(file)
return true return true
end end
local function scandir(dir, ignore_patterns, files) local function scandir(dir, ignore_patterns, files, conf)
local conf = config.load()
local fd = uv.fs_opendir(dir, nil, 50) local fd = uv.fs_opendir(dir, nil, 50)
if not fd then if not fd then
if conf.debug then if conf.debug then
@@ -78,11 +103,23 @@ local function scandir(dir, ignore_patterns, files)
if not ents then break end if not ents then break end
for _, ent in ipairs(ents) do for _, ent in ipairs(ents) do
local fullpath = dir .. "/" .. ent.name local fullpath = dir .. "/" .. ent.name
if not should_ignore_file(fullpath, ignore_patterns) then if not should_ignore_file(fullpath, ignore_patterns, conf) then
if ent.type == "file" and is_text_file(fullpath) then if ent.type == "file" and is_text_file(fullpath, conf) then
table.insert(files, fullpath) table.insert(files, fullpath)
elseif ent.type == "directory" and ent.name ~= ".git" then elseif ent.type == "directory" and ent.name ~= ".git" then
scandir(fullpath, ignore_patterns, files) scandir(fullpath, ignore_patterns, files, conf)
elseif ent.type == "link" then
local link_target = uv.fs_readlink(fullpath)
if link_target then
local st = uv.fs_stat(link_target)
if st and st.type == "directory" then
table.insert(files, fullpath .. " (symlink to directory " .. link_target .. ")")
else
table.insert(files, fullpath .. " (symlink to file " .. link_target .. ")")
end
else
table.insert(files, fullpath .. " (symlink)")
end
end end
end end
end end
@@ -91,17 +128,16 @@ local function scandir(dir, ignore_patterns, files)
return files return files
end end
function M.get_project_files(directories) function M.get_project_files(directories, conf)
local conf = config.load()
local root = vim.fn.getcwd() local root = vim.fn.getcwd()
local ignore_patterns = load_gitignore_patterns(root) local ignore_patterns = load_gitignore_patterns(root, conf)
local all_files = {} local all_files = {}
for _, dir in ipairs(directories) do for _, dir in ipairs(directories) do
local abs_dir = dir local abs_dir = dir
if not abs_dir:match("^/") then if not abs_dir:match("^/") then
abs_dir = root .. "/" .. dir abs_dir = root .. "/" .. dir
end end
scandir(abs_dir, ignore_patterns, all_files) scandir(abs_dir, ignore_patterns, all_files, conf)
end end
local rel_files = {} local rel_files = {}
@@ -117,14 +153,13 @@ function M.get_project_files(directories)
return rel_files return rel_files
end end
function M.get_project_structure(directories) function M.get_project_structure(directories, conf)
local files = M.get_project_files(directories) local files = M.get_project_files(directories, conf)
local structure = "Files:\n" .. table.concat(files, "\n") local structure = "Files:\n" .. table.concat(files, "\n")
return structure return structure
end end
function M.get_file_contents(files) function M.get_file_contents(files, conf)
local conf = config.load()
local root = vim.fn.getcwd() local root = vim.fn.getcwd()
local sections = {} local sections = {}
for _, f in ipairs(files) do for _, f in ipairs(files) do

View File

@@ -1,7 +1,9 @@
local M = {} local M = {}
local uv = vim.loop local uv = vim.loop
local config = require('chatgpt_nvim.config') -- Remove local config = require('chatgpt_nvim.config')
-- We'll accept conf from outside or init.lua
-- local config = require('chatgpt_nvim.config')
local function ensure_dir(path) local function ensure_dir(path)
local st = uv.fs_stat(path) local st = uv.fs_stat(path)
@@ -16,8 +18,7 @@ local function ensure_dir(path)
return true return true
end end
function M.get_clipboard_content() function M.get_clipboard_content(conf)
local conf = config.load()
local content = vim.fn.getreg('+') local content = vim.fn.getreg('+')
if conf.debug then if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:handler] Clipboard content length: " .. #content .. "\n") vim.api.nvim_out_write("[chatgpt_nvim:handler] Clipboard content length: " .. #content .. "\n")
@@ -25,8 +26,7 @@ function M.get_clipboard_content()
return content return content
end end
function M.write_file(filepath, content) function M.write_file(filepath, content, conf)
local conf = config.load()
local dir = filepath:match("(.*)/") local dir = filepath:match("(.*)/")
if dir and dir ~= "" then if dir and dir ~= "" then
ensure_dir(dir) ensure_dir(dir)
@@ -46,8 +46,7 @@ function M.write_file(filepath, content)
end end
end end
function M.delete_file(filepath) function M.delete_file(filepath, conf)
local conf = config.load()
local st = uv.fs_stat(filepath) local st = uv.fs_stat(filepath)
if st then if st then
local success, err = uv.fs_unlink(filepath) local success, err = uv.fs_unlink(filepath)

View File

@@ -4,15 +4,21 @@ local context = require('chatgpt_nvim.context')
local handler = require('chatgpt_nvim.handler') local handler = require('chatgpt_nvim.handler')
local config = require('chatgpt_nvim.config') 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 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
local function parse_response(raw) local function parse_response(raw, conf)
local conf = config.load()
if not ok_yaml then if not ok_yaml then
vim.api.nvim_err_writeln("lyaml not available. Install with `luarocks install lyaml`.") vim.api.nvim_err_writeln("lyaml not available. Install with `luarocks install lyaml`.")
return nil return nil
@@ -48,49 +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 estimate_tokens_basic(text)
local approx_chars_per_token = 4
local length = #text
return math.floor(length / approx_chars_per_token)
end
local function estimate_tokens_improved(text)
local words = #vim.split(text, "%s+")
local approximate_tokens = math.floor(words * 0.75)
ui.debug_log("Using improved token estimate: " .. approximate_tokens .. " tokens")
return approximate_tokens
end
local function get_estimate_fn()
local conf = config.load()
if conf.improved_debug then
return estimate_tokens_improved
else
return estimate_tokens_basic
end
end
local function handle_step_by_step_if_needed(prompt, estimate_fn)
local conf = config.load()
local token_count = estimate_fn(prompt)
if not conf.enable_step_by_step or token_count <= (conf.token_limit or 8000) then
return { prompt }
end
local step_prompt = [[
It appears this request might exceed the model's token limit if done all at once.
Please break down the tasks into smaller steps and handle them one by one.
At each step, we'll provide relevant files or context if needed.
Thank you!
]]
return { 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)
@@ -100,232 +63,105 @@ local function close_existing_buffer_by_name(pattern)
end end
end end
local function preview_changes(changes) ------------------------------------------------------------------------------
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") local lines = {}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { lines[#lines+1] = "<tools_available>"
"# Preview of Changes:", for _, t in ipairs(tools_module.available_tools) do
"# (Close this window to apply changes or use :q to cancel)", lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s",
"" t.name, t.explanation, t.usage
}) )
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
end lines[#lines+1] = "</tools_available>"
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" }) return table.concat(lines, "\n")
end end
vim.cmd("vsplit") local function build_prompt(user_input, dirs, conf)
vim.cmd("buffer " .. bufnr) 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 end
local function partial_accept(changes) -- 2) <tools_available>
close_existing_buffer_by_name("ChatGPT_Partial_Accept$") table.insert(final_sections, build_tools_available_block())
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 = { -- 3) <task>
"# Remove or comment out (prepend '#') any changes you do NOT want, then :wq, :x, or :bd to finalize partial acceptance", local task_lines = {}
"" task_lines[#task_lines+1] = "<task>"
} task_lines[#task_lines+1] = user_input
for _, fileinfo in ipairs(changes) do for _, file_path in ipairs(initial_files) do
local action = (fileinfo.delete == true) and "[DELETE]" or "[WRITE]" task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path)
table.insert(lines, string.format("%s %s", action, fileinfo.path or "<no path>")) end
if fileinfo.content then task_lines[#task_lines+1] = "</task>\n"
local content_lines = vim.split(fileinfo.content, "\n") table.insert(final_sections, table.concat(task_lines, "\n"))
for _, cl in ipairs(content_lines) do
table.insert(lines, " " .. cl) -- 4) <file_content path="...">
local file_content_blocks = {}
for _, file_path in ipairs(initial_files) do
local full_path = root .. "/" .. file_path
if is_subpath(root, full_path) then
local fdata = read_file(full_path)
if fdata then
file_content_blocks[#file_content_blocks+1] = string.format(
"<file_content path=\"%s\">\n%s\n</file_content>", file_path, fdata
)
end end
end end
table.insert(lines, "") end
if #file_content_blocks > 0 then
table.insert(final_sections, table.concat(file_content_blocks, "\n\n"))
end end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) -- 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"))
local final_changes = {} return table.concat(final_sections, "\n\n")
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 end
if keep_current and (current_fileinfo.path ~= nil) then local function handle_step_by_step_if_needed(prompt, conf)
if #content_accum > 0 then local length = #prompt
current_fileinfo.content = table.concat(content_accum, "\n") local limit = conf.prompt_char_limit or 8000
if (not conf.enable_step_by_step) or (length <= limit) then
return { prompt }
end end
table.insert(final_changes, current_fileinfo) return { prompts["step-prompt"] }
end end
vim.api.nvim_buf_set_option(bufnr, "modified", false) ------------------------------------------------------------------------------
end -- :ChatGPT
------------------------------------------------------------------------------
vim.api.nvim_create_autocmd("BufWriteCmd", { local function run_chatgpt_command()
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)
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
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 results = {}
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(results, filepath .. ":" .. line_num .. ":" .. line)
end
end
if #results == 0 then
return "No matches in " .. filepath
else
return table.concat(results, "\n")
end
end
local function execute_debug_command(cmd)
if type(cmd) ~= "table" or not cmd.command then
return "Invalid command object."
end
local command = cmd.command
if command == "ls" then
local dir = cmd.dir or "."
local handle = io.popen("ls " .. dir)
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
local pattern = cmd.pattern
local target = cmd.target
if not pattern or not target then
return "Usage for grep: {command='grep', pattern='<text>', target='<file_or_directory>'}"
end
local stat = vim.loop.fs_stat(target)
if not stat then
return "Cannot grep: target path does not exist"
end
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 = {}
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
return grep_in_file(pattern, target)
end
else
return "Unknown command: " .. command
end
end
function M.run_chatgpt_command()
local conf = config.load() local conf = config.load()
ui.setup_ui(conf)
ui.debug_log("Running :ChatGPT command.") ui.debug_log("Running :ChatGPT command.")
local dirs = conf.directories or {"."} local dirs = conf.directories or {"."}
if conf.interactive_file_selection then if conf.interactive_file_selection then
dirs = ui.pick_directories(dirs) dirs = ui.pick_directories(dirs, conf)
if #dirs == 0 then if #dirs == 0 then
dirs = conf.directories dirs = conf.directories
end end
@@ -340,7 +176,7 @@ function M.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."
}) })
@@ -350,73 +186,24 @@ function M.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) 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})
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, [[
If you need debugging commands, include them in your YAML response as follows:
```yaml
commands:
- command: "ls"
dir: "some/directory"
- command: "grep"
pattern: "searchString"
target: "path/to/file/or/directory"
```
The "ls" command uses the system's 'ls' command to list directory contents.
When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard.
]])
end
local prompt = table.concat(initial_sections, "\n")
store_prompt_for_reference(prompt)
local estimate_fn = get_estimate_fn()
local chunks = handle_step_by_step_if_needed(prompt, estimate_fn)
copy_to_clipboard(chunks[1]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
@@ -432,34 +219,41 @@ function M.run_chatgpt_command()
vim.cmd("buffer " .. bufnr) vim.cmd("buffer " .. bufnr)
end end
function M.run_chatgpt_paste_command() ------------------------------------------------------------------------------
-- :ChatGPTPaste
------------------------------------------------------------------------------
local function run_chatgpt_paste_command()
local conf = config.load() local conf = config.load()
ui.setup_ui(conf)
ui.debug_log("Running :ChatGPTPaste command.") ui.debug_log("Running :ChatGPTPaste command.")
print("Reading ChatGPT YAML response from clipboard...") print("Reading ChatGPT YAML response from clipboard...")
local raw = handler.get_clipboard_content() local raw = handler.get_clipboard_content(conf)
if raw == "" then if raw == "" then
vim.api.nvim_err_writeln("Clipboard is empty. Please copy the YAML response from ChatGPT first.") vim.api.nvim_err_writeln("Clipboard is empty. Please copy the YAML response from ChatGPT first.")
return return
end end
local data = parse_response(raw) local data = parse_response(raw, conf)
if not data then if not data then
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)) 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
@@ -475,7 +269,7 @@ function M.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) 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()
@@ -495,7 +289,7 @@ function M.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) 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
@@ -506,30 +300,28 @@ function M.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) handler.delete_file(fileinfo.path, conf)
print("Deleted: " .. fileinfo.path) print("Deleted: " .. fileinfo.path)
elseif fileinfo.content then elseif fileinfo.content then
ui.debug_log("Writing file: " .. fileinfo.path) ui.debug_log("Writing file: " .. fileinfo.path)
handler.write_file(fileinfo.path, fileinfo.content) handler.write_file(fileinfo.path, fileinfo.content, conf)
print("Wrote: " .. fileinfo.path) print("Wrote: " .. fileinfo.path)
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)
@@ -537,126 +329,76 @@ function M.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
local sections = { local sections = {
config.load().initial_prompt, conf.initial_prompt,
"\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")
local estimate_fn = get_estimate_fn() local length = #prompt
local token_count = estimate_fn(prompt) ui.debug_log("Returning requested files. Character count: " .. length)
ui.debug_log("Returning requested files. Token count: " .. token_count)
if token_count > (conf.token_limit or 8000) and conf.enable_step_by_step then if length > (conf.prompt_char_limit or 8000) and conf.enable_step_by_step then
local step_message = [[ local large_step = prompts["step-prompt"]
It appears this requested data is quite large. Please split the task into smaller steps copy_to_clipboard(large_step)
and continue step by step.
Which files would you need for the first step?
]]
copy_to_clipboard(step_message)
print("Step-by-step guidance copied to clipboard!") print("Step-by-step guidance copied to clipboard!")
return return
elseif token_count > (conf.token_limit or 8000) then elseif length > (conf.prompt_char_limit or 8000) then
vim.api.nvim_err_writeln("Requested files exceed token limit. No step-by-step support enabled.") vim.api.nvim_err_writeln("Requested files exceed prompt character limit. No step-by-step support enabled.")
return return
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
function M.run_chatgpt_current_buffer_command() ------------------------------------------------------------------------------
-- :ChatGPTCurrentBuffer
------------------------------------------------------------------------------
local function run_chatgpt_current_buffer_command()
local conf = config.load() local conf = config.load()
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 {"."}
if conf.interactive_file_selection then if conf.interactive_file_selection then
dirs = ui.pick_directories(dirs) dirs = ui.pick_directories(dirs, conf)
if #dirs == 0 then if #dirs == 0 then
dirs = conf.directories dirs = conf.directories
end end
end end
local project_structure = context.get_project_structure(dirs) 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})
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, [[
If you need debugging commands, include them in your YAML response as follows:
```yaml
commands:
- command: "list"
dir: "some/directory"
- command: "grep"
pattern: "searchString"
target: "path/to/file/or/directory"
```
The "list" command uses the system's 'ls' command to list directory contents.
When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard.
]])
end
local prompt = table.concat(initial_sections, "\n")
store_prompt_for_reference(prompt)
local estimate_fn = get_estimate_fn()
local chunks = handle_step_by_step_if_needed(prompt, estimate_fn)
copy_to_clipboard(chunks[1]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
@@ -666,4 +408,11 @@ function M.run_chatgpt_current_buffer_command()
end end
end end
------------------------------------------------------------------------------
-- PUBLIC API
------------------------------------------------------------------------------
M.run_chatgpt_command = run_chatgpt_command
M.run_chatgpt_paste_command = run_chatgpt_paste_command
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
return M return M

View File

@@ -0,0 +1,367 @@
local M = {
["solidjs-development"] = [[
### SolidJS Development Guidelines
You are helping me develop a large SolidJS application. Please keep the following points in mind when generating or explaining code:
1. **Project & Folder Structure**
- Use a modern bundler/build tool (e.g., [Vite](https://vitejs.dev/)) with SolidJS support.
- Maintain a clear, top-level directory layout, typically:
```
my-solid-app/
├── public/
├── src/
│ ├── components/
│ ├── pages/
│ ├── routes/ (if using a router)
│ ├── store/ (for signals/contexts)
│ ├── styles/
│ └── index.tsx
├── package.json
├── vite.config.ts
└── tsconfig.json
```
- Organize common UI elements (buttons, modals, etc.) in `src/components/`, separate page-level views in `src/pages/`, and keep global or shared state in `src/store/`.
2. **SolidJS Reactivity & State Management**
- Use **signals**, **memos**, and **effects** judiciously:
- `createSignal` for local state,
- `createEffect` for reactive computations and side effects,
- `createMemo` to cache expensive computations.
- For global or cross-component state, use **Context** providers or a dedicated store pattern (with signals/contexts).
- Avoid unnecessary reactivity—structure signals to update only when needed.
3. **Routing & Navigation**
- Leverage the official [Solid Router](https://github.com/solidjs/solid-router) or [Solid Start](https://start.solidjs.com/) for routing.
- Keep routes organized, especially in large projects (e.g., a dedicated `routes/` folder or a route config file).
- Support code splitting with dynamic imports where appropriate for faster initial loads.
4. **Component & Code Organization**
- Name components in **PascalCase** (e.g., `NavBar.tsx`, `UserProfile.tsx`), and keep them focused on a single responsibility.
- Co-locate component-specific styles, tests, and other assets alongside the component file to simplify discovery (`MyComponent/` folder pattern).
- Encourage **reuse** by factoring out small, generic components from more complex ones.
5. **Styling & CSS Management**
- Use your preferred styling approach (CSS Modules, [Tailwind CSS](https://tailwindcss.com/), or standard CSS/SCSS files).
- Keep global styles minimal, focusing on utility classes or base styling; keep component-level styles scoped whenever possible.
- If using CSS-in-JS solutions or third-party libraries, ensure they integrate cleanly with Solids reactivity.
6. **TypeScript & Linting**
- Use **TypeScript** to ensure type safety and improve maintainability.
- Include a strict `tsconfig.json` configuration (e.g., `"strict": true`).
- Employ linting and formatting tools:
- [ESLint](https://eslint.org/) with the [eslint-plugin-solid](https://github.com/solidjs-community/eslint-plugin-solid)
- [Prettier](https://prettier.io/) for consistent formatting
- Run these tools in a pre-commit hook or CI pipeline to maintain code quality.
7. **Testing & Quality Assurance**
- Write **unit tests** for smaller components with a testing library like [Vitest](https://vitest.dev/) or [Jest](https://jestjs.io/) (configured for SolidJS).
- For **integration or end-to-end (E2E) tests**, use tools like [Cypress](https://www.cypress.io/) or [Playwright](https://playwright.dev/).
- Aim for a robust CI workflow that runs tests automatically on every commit or pull request.
8. **Performance & Optimization**
- SolidJS is already performant with granular reactivity, but still follow best practices:
- Avoid creating signals/memos/effects in tight loops or repeatedly in render.
- Use code splitting and lazy loading for large features or pages.
- Leverage caching and memoization for expensive computations.
- Profile your app with dev tools (e.g., [Solid DevTools](https://github.com/thetarnav/solid-devtools)) to identify and address performance bottlenecks.
9. **Output Format**
- Present any generated source code as well-organized files under the appropriate folders (e.g., `components/`, `pages/`).
- When explaining your reasoning, include any architectural or design trade-offs (e.g., “I created a separate store signal for authentication to isolate login logic from other features.”).
- If you modify existing files, specify precisely which lines or sections have changed, and why.
Please follow these guidelines to ensure the generated or explained code aligns well with SolidJS best practices for large, maintainable projects.
]],
["go-development"] = [[
### Go Development Guidelines
You are helping me develop a large Go (Golang) project. Please keep the following points in mind when generating or explaining code:
1. **Go Modules**
- Use a single `go.mod` file at the project root for module management.
- Ensure you use proper import paths based on the module name.
- If you refer to internal packages, use relative paths consistent with the modules structure (e.g., `moduleName/internal/packageA`).
2. **Package Structure**
- Each folder should contain exactly one package.
- Avoid creating multiple packages in the same folder.
- Use descriptive folder names and keep package names concise, following Go naming conventions.
- Do not duplicate function or type names across different files in the same folder/package.
3. **File & Folder Organization**
- Organize source code in a folder hierarchy that reflects functionality. For example:
```
myproject/
├── go.mod
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── service/
│ ├── repository/
│ └── ...
└── pkg/
└── shared/
```
- Keep external-facing, reusable packages in `pkg/` and internal logic in `internal/`.
- Place the `main()` function in the `cmd/<appname>` folder.
4. **Coding Best Practices**
- Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols).
- Keep functions short, focused, and tested.
- Use Gos standard library where possible before adding third-party dependencies.
- When introducing new functions or types, ensure they are uniquely named to avoid collisions.
5. **Import Management**
- Ensure that every import is actually used in your code.
- Remove unused imports to keep your code clean and maintainable.
- Include all necessary imports for anything referenced in your code to avoid missing imports.
- Verify that any introduced import paths match your modules structure and do not cause naming conflicts.
6. **Output Format**
- Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule.
- When explaining your reasoning, include any relevant architectural trade-offs and rationale (e.g., “I placed function X in package Y to keep the domain-specific logic separate from the main execution flow.”).
- If you modify an existing file, specify precisely which changes or additions you are making.
Please follow these guidelines to ensure the generated or explained code aligns well with Golangs best practices for large, modular projects.
]],
["typo3-development"] = [[
### TYPO3 Development Guidelines
You are helping me develop a large TYPO3 project. Please keep the following points in mind when generating or explaining code:
1. **Project & Folder Structure**
- Organize the project to take advantage of Composer-based installation (e.g., `composer.json` referencing `typo3/cms-core` and any additional extensions).
- Maintain a clean, well-defined folder hierarchy, typically:
```
project-root/
├── composer.json
├── public/
│ ├── fileadmin/
│ ├── typo3/
│ └── typo3conf/
├── config/
│ └── sites/
├── var/
└── ...
```
- Store custom extensions inside `public/typo3conf/ext/` or use Composer to manage them under `vendor/`.
- Keep site configuration in `config/sites/` (for TYPO3 v9+).
2. **Extension Development**
- Create custom functionality as separate extensions (site packages, domain-specific extensions, etc.) following TYPO3s recommended structure:
- **Key files**: `ext_emconf.php`, `ext_localconf.php`, `ext_tables.php`, `ext_tables.sql`, `Configuration/`, `Classes/`, `Resources/`.
- Use **PSR-4** autoloading and name extensions logically (e.g., `my_sitepackage`, `my_blogextension`).
- Keep your extensions code under `Classes/` (e.g., Controllers, Models, Services).
- Place Fluid templates, partials, and layouts under `Resources/Private/` (e.g., `Resources/Private/Templates`, `Resources/Private/Partials`, `Resources/Private/Layouts`).
3. **Configuration (TypoScript & TCA)**
- Maintain **TypoScript** files in a clear, hierarchical structure under `Configuration/TypoScript/` or `Resources/Private/TypoScript/`.
- Use **ext_typoscript_setup.txt** and **ext_typoscript_constants.txt** (or `.typoscript` alternatives) for extension-wide configuration.
- Define **TCA** for custom database tables or custom fields in `Configuration/TCA` or in `ext_tables.php/ext_localconf.php` as needed.
- Respect naming conventions for database tables and fields to keep them unique to your extension (e.g., `tx_myextension_domain_model_xyz`).
4. **Coding Standards & Best Practices**
- Follow **PSR-2/PSR-12** coding standards for PHP (naming, indentation, etc.).
- Keep classes short, focused, and well-documented with PHPDoc comments.
- Separate concerns:
- Controllers handle request logic,
- Models represent data,
- Repositories abstract data access,
- Services implement business logic.
- Avoid placing large chunks of code in Fluid templates—keep logic in PHP classes and pass the data to templates.
- Use official TYPO3 APIs (e.g., `GeneralUtility`, `Context` API) where applicable rather than raw PHP or legacy code.
5. **Fluid Templates & Rendering**
- Keep template files in `Resources/Private/Templates/<ControllerName>/<ActionName>.html`.
- Use `Resources/Private/Partials` and `Resources/Private/Layouts` to avoid duplicating common template sections.
- Register your templates, partials, and layouts in TypoScript or in `Configuration/Services.yaml` for automatic discovery.
6. **Documentation & Version Control**
- Write README/CHANGELOG files at the extension root to describe major changes and usage.
- Include inline documentation and use Git for version control.
- Adhere to **Semantic Versioning** (`MAJOR.MINOR.PATCH`) for your custom extensions, when applicable.
7. **Output Format**
- Present any generated source code or configuration files in a well-organized structure.
- Clearly indicate where each file should be placed in the TYPO3 directory layout.
- When explaining your reasoning, include any relevant architectural decisions (e.g., “I created a separate extension for blog functionality to keep it isolated from the sites main configuration.”).
- If you modify or extend an existing file, specify precisely which changes or additions you are making.
Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3s best practices for large, maintainable projects.
]],
["rust-development"] = [[
### Rust Development Guidelines
You are helping me develop a large Rust project. Please keep the following points in mind when generating or explaining code:
1. **Cargo & Workspace Management**
- Use a [Cargo workspace](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) for managing multiple crates under one top-level project.
- Maintain a `Cargo.toml` at the workspace root, referencing the member crates in the `[workspace]` section.
- Keep dependency versions up-to-date and consistent across crates.
2. **Crates & Packages**
- Split the application into logical crates (libraries and/or binaries).
- Each crate should have a single main **library** (`lib.rs`) or **binary** (`main.rs`) in its `src/` folder.
- Name crates, modules, and files clearly, following Rusts naming conventions (e.g., `snake_case` for files/modules, `PascalCase` for types).
- Avoid duplicating the same function or type in multiple crates; share common functionality via a dedicated library crate if needed.
3. **Folder & Module Structure**
- Organize code within each crate using Rusts module system, keeping related functions and types in logical modules/submodules.
- A typical directory layout for a workspace with multiple crates might look like:
```
myproject/
├── Cargo.toml # Workspace root
├── crates/
│ ├── my_lib/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ └── ...
│ └── my_app/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── target/
└── ...
```
- If you have integration tests, store them in a `tests/` folder at the crate root, or use the workspace roots `tests/` directory if they span multiple crates.
4. **Coding & Documentation Best Practices**
- Write **idiomatic Rust** code:
- Use `cargo fmt` (formatting) and `cargo clippy` (linter) to maintain consistency and quality.
- Use `?` operator for error handling, prefer `Result<T, E>` over panicking unless absolutely necessary.
- Document your code using [Rustdoc](https://doc.rust-lang.org/rustdoc/) comments (`///` for public API) and provide examples when relevant.
- Write **unit tests** alongside the code (in `src/` files) and **integration tests** in a dedicated `tests/` folder.
- Keep functions short, focused, and ensure they have well-defined responsibilities.
5. **Reusability & Shared Code**
- Place common or reusable functionality into a dedicated **library** crate.
- Ensure that crates depending on shared code add the appropriate `[dependencies]` or `[dev-dependencies]` in their `Cargo.toml`.
- Use the Rust standard library whenever possible before introducing external dependencies.
6. **Error Handling & Logging**
- Use structured, typed error handling (e.g., [thiserror](https://crates.io/crates/thiserror) or [anyhow](https://crates.io/crates/anyhow)) for more readable error management if appropriate.
- Provide clear, contextual error messages that help in debugging.
- Include robust logging with a minimal overhead library (e.g., [log](https://crates.io/crates/log) with [env_logger](https://crates.io/crates/env_logger) or similar).
7. **Output Format**
- Present generated source code as well-organized Rust files, respecting the single main library or binary per crate (`lib.rs` or `main.rs`).
- When explaining your reasoning, include any architectural or design decisions (e.g., “I placed function X in crate `my_lib` to keep the business logic separate from the command-line interface.”).
- If you modify existing files, specify precisely which lines or sections have changed.
Please follow these guidelines to ensure the generated or explained code aligns well with Rust best practices for large, modular projects.
]],
["basic-prompt"] = [[
### Basic Prompt
You are assisting me in a coding workflow for a project (e.g., "my_project"). Whenever you need to inspect or modify files, or execute commands, you must provide both:
1. `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
project_name: "my_project"
tools:
- tool: "readFile"
path: "relative/path/to/file"
- tool: "replace_in_file"
path: "relative/path/to/file"
replacements:
- search: "old text"
replace: "new text"
- tool: "editFile"
path: "relative/path/to/file"
content: |
# Full updated file content here
- tool: "executeCommand"
command: "ls -la"
```
**Key Points**:
- 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.
- If multiple tools are needed, list them sequentially in the `tools` array.
- Always run at least one tool (e.g., `readFile`, `editFile`, `executeCommand`), exept you have finished.
- Always just include one yaml in the response with all the tools you want to run in that yaml.
- The plugin will verify the `project_name` is correct before running any tools.
- If the response grows too large, I'll guide you to break it into smaller steps.
]],
["secure-coding"] = [[
### Secure Coding Guidelines
You are assisting me in creating software that prioritizes security at every stage of the development process. Please adhere to the following guidelines whenever you propose code, architecture, or any form of implementation detail:
1. **Secure Coding Principles**
- Emphasize **input validation**, **output encoding**, and **context-aware sanitization** (e.g., sanitizing user inputs against SQL injection, XSS, CSRF, etc.).
- Follow the principle of **least privilege**:
- Only request or grant the permissions necessary for each components functionality.
- Implement robust **error handling** and **logging** without revealing sensitive data (e.g., do not log passwords, tokens, or PII).
2. **OWASP Top Ten Alignment**
- Consult the **OWASP Top Ten** (or equivalent security framework) to address common risks:
- **Injection** (SQL, NoSQL, Command Injection)
- **Broken Authentication**
- **Sensitive Data Exposure**
- **XML External Entities**
- **Broken Access Control**
- **Security Misconfiguration**
- **Cross-Site Scripting (XSS)**
- **Insecure Deserialization**
- **Using Components with Known Vulnerabilities**
- **Insufficient Logging & Monitoring**
- Provide secure defaults and demonstrate how to mitigate each risk using example code.
3. **Secure Communication & Data Handling**
- Use **TLS/SSL** for all data in transit wherever possible.
- Store sensitive data in encrypted form, leveraging secure, up-to-date cryptographic libraries.
- Avoid hardcoding credentials or secrets in source code. Use secure secrets management solutions.
4. **Dependency & Third-Party Library Management**
- Use only reputable, **actively maintained** third-party libraries and verify they have no known critical vulnerabilities.
- Keep all dependencies **updated** to minimize exposure to known security flaws.
5. **Authentication & Authorization**
- Implement **secure authentication flows** (e.g., token-based authentication, OAuth2, OpenID Connect).
- Use robust **password hashing** algorithms such as bcrypt, scrypt, or Argon2 (avoid MD5 or SHA1).
- Enforce **strong password** or credential policies.
- Implement **role-based access control (RBAC)** or attribute-based access control (ABAC) where appropriate.
6. **Secure Configuration**
- Apply **secure configuration defaults** (e.g., disable unnecessary services, secure admin endpoints, etc.).
- Avoid exposing internal ports or services to the public network.
- Use a **Content Security Policy (CSP)** header, secure cookies, and other HTTP security headers where relevant.
7. **Secure Deployment & Maintenance**
- Include guidelines for **monitoring** (e.g., intrusion detection, anomaly detection).
- Provide methods for **logging** security-relevant events (failed logins, data access anomalies).
- Outline **incident response** steps (e.g., notifications, rollback procedures, quick patching, etc.).
8. **Documentation & Continuous Improvement**
- Document all security-related decisions and configurations.
- Encourage periodic **code reviews** and **security audits**.
- Support **continuous integration and deployment (CI/CD)** pipelines with automated security checks (static analysis, dependency scanning, etc.).
9. **Output Format**
- Present any generated source code with **secure defaults** and thorough in-code commentary about security measures.
- If you propose changes to existing code or configurations, specify precisely how youve enhanced security.
- Whenever possible, provide references to relevant security standards, best practices, or guidelines (e.g., OWASP, NIST).
Please follow these guidelines to ensure the generated or explained code prioritizes security at every level, mitigating potential risks and maintaining best practices for building secure software.
]],
["step-prompt"] = [[
It appears this request might exceed the model's prompt character limit if done all at once.
Please break down the tasks into smaller steps and handle them one by one.
Thank you!
]],
["file-selection-instructions"] = [[
Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd)
]]
}
return M

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

View File

@@ -1,40 +1,49 @@
local M = {} local M = {}
local config = require('chatgpt_nvim.config') -- Remove local conf = config.load()
local conf = config.load() -- We'll have each function accept 'conf' from outside or handle it in init.lua
-- local config = require('chatgpt_nvim.config')
-- local conf = config.load()
local prompts = require('chatgpt_nvim.prompts')
local debug_bufnr = nil local debug_bufnr = nil
if conf.improved_debug then
-- Check if a debug buffer is already open. Close it first to avoid duplicates. -- We'll store a reference to conf at runtime
local runtime_conf = nil
function M.setup_ui(conf)
runtime_conf = conf
if runtime_conf.improved_debug then
for _, buf in ipairs(vim.api.nvim_list_bufs()) do for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(buf) local name = vim.api.nvim_buf_get_name(buf)
if name == "ChatGPT_Debug_Log" then if name == "ChatGPT_Debug_Log" then
vim.api.nvim_buf_delete(buf, {force = true}) vim.api.nvim_buf_delete(buf, {force = true})
end end
end end
debug_bufnr = vim.api.nvim_create_buf(false, true) debug_bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(debug_bufnr, "ChatGPT_Debug_Log") vim.api.nvim_buf_set_name(debug_bufnr, "ChatGPT_Debug_Log")
vim.api.nvim_buf_set_option(debug_bufnr, "filetype", "log") vim.api.nvim_buf_set_option(debug_bufnr, "filetype", "log")
end end
end
function M.debug_log(msg) function M.debug_log(msg)
if conf.improved_debug and debug_bufnr then if runtime_conf and runtime_conf.improved_debug and debug_bufnr then
vim.api.nvim_buf_set_lines(debug_bufnr, -1, -1, false, { msg }) vim.api.nvim_buf_set_lines(debug_bufnr, -1, -1, false, { msg })
else else
if conf.debug then if runtime_conf and runtime_conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:debug] " .. msg .. "\n") vim.api.nvim_out_write("[chatgpt_nvim:debug] " .. msg .. "\n")
end end
end end
end end
function M.pick_directories(dirs) function M.pick_directories(dirs, conf)
local selected_dirs = {} local selected_dirs = {}
local lines = { "Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd)" } local selection_instructions = prompts["file-selection-instructions"]
local lines = { selection_instructions }
for _, d in ipairs(dirs) do for _, d in ipairs(dirs) do
table.insert(lines, d) table.insert(lines, d)
end end
-- If a file selection buffer is already open, close it to avoid confusion
for _, buf in ipairs(vim.api.nvim_list_bufs()) do for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(buf) local name = vim.api.nvim_buf_get_name(buf)
if name:match("ChatGPT_File_Selection") then if name:match("ChatGPT_File_Selection") then
@@ -65,7 +74,6 @@ function M.pick_directories(dirs)
once = true, once = true,
callback = function() callback = function()
on_write() on_write()
-- Automatically close the buffer once saved
vim.cmd("bd! " .. bufnr) vim.cmd("bd! " .. bufnr)
end end
}) })
@@ -73,7 +81,6 @@ function M.pick_directories(dirs)
vim.cmd("split") vim.cmd("split")
vim.cmd("buffer " .. bufnr) vim.cmd("buffer " .. bufnr)
-- Wait up to 30s for user to close
vim.wait(30000, function() vim.wait(30000, function()
local winids = vim.api.nvim_tabpage_list_wins(0) local winids = vim.api.nvim_tabpage_list_wins(0)
for _, w in ipairs(winids) do for _, w in ipairs(winids) do
@@ -88,8 +95,4 @@ function M.pick_directories(dirs)
return selected_dirs return selected_dirs
end end
--------------------------------------------------------------------------------
-- The old chunkify method has been removed, since we now rely on step-by-step
--------------------------------------------------------------------------------
return M return M