Compare commits
19 Commits
bae7d106ac
...
change-to-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aa00813cf | |||
| 58da08e26f | |||
| 35bd0a7278 | |||
| 0ff77954db | |||
| fd8df2abd5 | |||
| 59540981ed | |||
| e36ad99dcb | |||
| f7d986f5fc | |||
| b7274a310b | |||
| 904979b0f6 | |||
| b9643952cb | |||
| 1520641a04 | |||
| f1cc371294 | |||
| ca729140c3 | |||
| 6950c542ad | |||
| 3505a295a9 | |||
| 2c855e881b | |||
| 452253cdd0 | |||
| a77dbb683d |
@@ -1,17 +1,27 @@
|
||||
project_name: "chatgpt_nvim"
|
||||
default_prompt_blocks:
|
||||
- "basic-prompt"
|
||||
- "workflow-prompt"
|
||||
directories:
|
||||
- "."
|
||||
- "secure-coding"
|
||||
initial_files:
|
||||
- "README.md"
|
||||
|
||||
debug: false
|
||||
improved_debug: false
|
||||
|
||||
preview_changes: false
|
||||
interactive_file_selection: false
|
||||
partial_acceptance: false
|
||||
improved_debug: false
|
||||
|
||||
enable_debug_commands: true
|
||||
token_limit: 128000
|
||||
prompt_char_limit: 300000
|
||||
enable_chunking: false
|
||||
enable_step_by_step: true
|
||||
auto_lint: true
|
||||
|
||||
# New tool auto-accept config
|
||||
tool_auto_accept:
|
||||
readFile: false
|
||||
editFile: false
|
||||
executeCommand: false
|
||||
# If you set any of these to true, it will auto accept them without prompting.
|
||||
# 'executeCommand' should remain false by default unless you're certain it's safe.
|
||||
|
||||
38
README.md
38
README.md
@@ -1,5 +1,5 @@
|
||||
<!-- README.md -->
|
||||
# ChatGPT NeoVim Plugin (Extensively Updated with Step-by-Step Prompting and Diff-Based Changes)
|
||||
# ChatGPT NeoVim Plugin (Extensively Updated with Step-by-Step Prompting)
|
||||
|
||||
This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to:
|
||||
|
||||
@@ -10,26 +10,21 @@ This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to
|
||||
- Any **initial files** you define (e.g., `README.md`, etc.)
|
||||
|
||||
2. Copy these prompts to your clipboard to paste into ChatGPT O1.
|
||||
3. Receive **YAML changes from ChatGPT that include diffs**, then run `:ChatGPTPaste` to apply them or supply additional files.
|
||||
- If you’re updating an existing file, provide a `diff` field in the YAML.
|
||||
- If you’re creating a new file, use the `content` field.
|
||||
- If you’re deleting a file, use `delete: true`.
|
||||
3. Receive YAML changes from ChatGPT, then run `:ChatGPTPaste` to apply them or supply additional files.
|
||||
|
||||
## New Key Features
|
||||
|
||||
- **Step-by-Step Prompting** (`enable_step_by_step: true`):
|
||||
If the request grows too large (exceeds `token_limit`), the plugin automatically generates a special prompt asking the model to split the task into smaller steps, working through them one by one.
|
||||
If the request grows too large (exceeds `token_limit`), the plugin automatically generates a special prompt asking the model to split the task into smaller steps, working through them one by one. This approach helps you stay within the model’s maximum token limit without having to manually break things apart.
|
||||
|
||||
- **Partial Acceptance**: If `partial_acceptance: true`, you can open a buffer that lists final diffs or file creations. You can remove or comment out lines you don’t want, then only those changes are applied.
|
||||
- **Partial Acceptance**: If `partial_acceptance: true`, you can open a buffer that lists the final changes. Remove or comment out lines you don’t want, then only those changes are applied.
|
||||
|
||||
- **Preview Changes**: If `preview_changes: true`, you get a buffer showing proposed diffs or new file content before you apply them.
|
||||
- **Preview Changes**: If `preview_changes: true`, you get a buffer showing proposed changes before you apply them.
|
||||
|
||||
- **Interactive File Selection**: If `interactive_file_selection: true`, you choose which directories from `.chatgpt_config.yaml` get included in the prompt, reducing token usage.
|
||||
|
||||
- **Improved Debug**: If `improved_debug: true`, debug logs go into a dedicated `ChatGPT_Debug_Log` buffer for easier reading.
|
||||
|
||||
- **Diff-Based Changes**: Rather than supplying entire file content for edits, you can include a `diff` in the YAML response. This allows you to see exactly what changed line by line and accept or reject it.
|
||||
|
||||
## Example `.chatgpt_config.yaml`
|
||||
|
||||
```yaml
|
||||
@@ -54,7 +49,7 @@ token_limit: 3000
|
||||
|
||||
1. **`:ChatGPT`**
|
||||
- If `interactive_file_selection` is on, you’ll pick directories to include in a buffer named `ChatGPT_File_Selection`.
|
||||
- Save & close with `:wq`, `:x`, or `:bd`.
|
||||
- Save & close with `:wq`, `:x`, or `:bd` (you don’t have to use `:q`).
|
||||
- If `enable_step_by_step` is on and the prompt might exceed `token_limit`, the plugin will generate instructions prompting the model to address each step separately.
|
||||
|
||||
2. **Paste Prompt to ChatGPT**
|
||||
@@ -65,7 +60,7 @@ token_limit: 3000
|
||||
- If final changes are provided:
|
||||
- Optionally preview them (`preview_changes`).
|
||||
- Optionally partially accept them (`partial_acceptance`).
|
||||
- The plugin then applies file creation or deletion, or applies diffs to existing files.
|
||||
- Then the plugin writes/deletes files as specified.
|
||||
|
||||
## Troubleshooting & Tips
|
||||
- Adjust `token_limit` in `.chatgpt_config.yaml` as needed.
|
||||
@@ -85,6 +80,21 @@ commands:
|
||||
pattern: "searchString"
|
||||
target: "path/to/file/or/directory"
|
||||
```
|
||||
The **list** command uses the system's `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!
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
Enjoy the improved, more flexible ChatGPT Neovim plugin with step-by-step and diff-based support!
|
||||
|
||||
@@ -2,53 +2,7 @@ local M = {}
|
||||
local uv = vim.loop
|
||||
|
||||
local ok_yaml, lyaml = pcall(require, "lyaml")
|
||||
|
||||
local prompt_blocks = {
|
||||
["go-development"] = [[
|
||||
You are a coding assistant specialized in Go development.
|
||||
You will receive a project’s context and user instructions related to Go code.
|
||||
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 project’s context and the user’s 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 project’s context and user instructions related to Rust code.
|
||||
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. Analyse which files you need. Ask for file contents in YAML if needed.
|
||||
2. Request additional context outside YAML if necessary.
|
||||
3. When ready, provide final changes in YAML with:
|
||||
- 'project_name'
|
||||
- 'files', each having:
|
||||
* 'path'
|
||||
* 'diff' (for patching an existing file) OR 'content' (for a new file) OR 'delete'
|
||||
Important: do not provide entire file content for updates; instead provide a unified diff in 'diff'.
|
||||
Only modify or delete files whose contents you have explicitly requested and seen beforehand.
|
||||
]],
|
||||
["secure-coding"] = [[
|
||||
You are a coding assistant specialized in secure software development.
|
||||
Always consider security impacts. Use diffs for updates, new content for new files,
|
||||
and 'delete: true' for removals.
|
||||
]],
|
||||
["workflow-prompt"] = [[
|
||||
You are a coding assistant focusing on making the Neovim ChatGPT workflow straightforward and user-friendly.
|
||||
Remind the user to:
|
||||
- List needed files for further context
|
||||
- Request additional information outside YAML if needed
|
||||
- Provide final changes in YAML with 'project_name' and 'files', using:
|
||||
* 'diff' for existing file modifications
|
||||
* 'content' for new files
|
||||
* 'delete: true' for file deletions
|
||||
]]
|
||||
}
|
||||
local prompts = require("chatgpt_nvim.prompts")
|
||||
|
||||
local function get_project_root()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
@@ -86,7 +40,7 @@ function M.load()
|
||||
initial_prompt = "",
|
||||
directories = { "." },
|
||||
default_prompt_blocks = {},
|
||||
token_limit = 16384,
|
||||
prompt_char_limit = 300000,
|
||||
project_name = "",
|
||||
debug = false,
|
||||
initial_files = {},
|
||||
@@ -96,7 +50,16 @@ function M.load()
|
||||
improved_debug = false,
|
||||
enable_chunking = false,
|
||||
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
|
||||
@@ -115,8 +78,8 @@ function M.load()
|
||||
if type(result.default_prompt_blocks) == "table" then
|
||||
config.default_prompt_blocks = result.default_prompt_blocks
|
||||
end
|
||||
if type(result.token_limit) == "number" then
|
||||
config.token_limit = result.token_limit
|
||||
if type(result.prompt_char_limit) == "number" then
|
||||
config.prompt_char_limit = result.prompt_char_limit
|
||||
end
|
||||
if type(result.project_name) == "string" then
|
||||
config.project_name = result.project_name
|
||||
@@ -145,8 +108,18 @@ function M.load()
|
||||
if type(result.enable_step_by_step) == "boolean" then
|
||||
config.enable_step_by_step = result.enable_step_by_step
|
||||
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
|
||||
@@ -154,11 +127,12 @@ function M.load()
|
||||
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
|
||||
end
|
||||
|
||||
-- Merge default prompt blocks
|
||||
if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
|
||||
local merged_prompt = {}
|
||||
for _, block_name in ipairs(config.default_prompt_blocks) do
|
||||
if prompt_blocks[block_name] then
|
||||
table.insert(merged_prompt, prompt_blocks[block_name])
|
||||
if prompts[block_name] then
|
||||
table.insert(merged_prompt, prompts[block_name])
|
||||
end
|
||||
end
|
||||
if #merged_prompt > 0 then
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
local M = {}
|
||||
|
||||
local uv = vim.loop
|
||||
local config = require('chatgpt_nvim.config')
|
||||
|
||||
local function load_gitignore_patterns(root)
|
||||
local conf = config.load()
|
||||
-- Converts a .gitignore-style pattern to a Lua pattern
|
||||
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 fd = uv.fs_open(gitignore_path, "r", 438)
|
||||
if not fd then
|
||||
@@ -19,9 +48,9 @@ local function load_gitignore_patterns(root)
|
||||
if not data then return {} end
|
||||
local patterns = {}
|
||||
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
|
||||
patterns[#patterns+1] = line
|
||||
table.insert(patterns, gitignore_to_lua_pattern(line))
|
||||
end
|
||||
end
|
||||
if conf.debug then
|
||||
@@ -30,10 +59,9 @@ local function load_gitignore_patterns(root)
|
||||
return patterns
|
||||
end
|
||||
|
||||
local function should_ignore_file(file, ignore_patterns)
|
||||
local conf = config.load()
|
||||
local function should_ignore_file(file, ignore_patterns, conf)
|
||||
for _, pattern in ipairs(ignore_patterns) do
|
||||
if file:find(pattern, 1, true) then
|
||||
if file:match(pattern) then
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Ignoring file/dir: " .. file .. " (matched pattern: " .. pattern .. ")\n")
|
||||
end
|
||||
@@ -43,8 +71,7 @@ local function should_ignore_file(file, ignore_patterns)
|
||||
return false
|
||||
end
|
||||
|
||||
local function is_text_file(file)
|
||||
local conf = config.load()
|
||||
local function is_text_file(file, conf)
|
||||
local fd = uv.fs_open(file, "r", 438)
|
||||
if not fd then
|
||||
if conf.debug then
|
||||
@@ -54,7 +81,6 @@ local function is_text_file(file)
|
||||
end
|
||||
local chunk = uv.fs_read(fd, 1024, 0) or ""
|
||||
uv.fs_close(fd)
|
||||
-- Check for null bytes as a heuristic for binary files
|
||||
if chunk:find("\0") then
|
||||
if conf.debug then
|
||||
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
|
||||
end
|
||||
|
||||
local function scandir(dir, ignore_patterns, files)
|
||||
local conf = config.load()
|
||||
local function scandir(dir, ignore_patterns, files, conf)
|
||||
local fd = uv.fs_opendir(dir, nil, 50)
|
||||
if not fd then
|
||||
if conf.debug then
|
||||
@@ -78,11 +103,23 @@ local function scandir(dir, ignore_patterns, files)
|
||||
if not ents then break end
|
||||
for _, ent in ipairs(ents) do
|
||||
local fullpath = dir .. "/" .. ent.name
|
||||
if not should_ignore_file(fullpath, ignore_patterns) then
|
||||
if ent.type == "file" and is_text_file(fullpath) then
|
||||
if not should_ignore_file(fullpath, ignore_patterns, conf) then
|
||||
if ent.type == "file" and is_text_file(fullpath, conf) then
|
||||
table.insert(files, fullpath)
|
||||
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
|
||||
@@ -91,17 +128,16 @@ local function scandir(dir, ignore_patterns, files)
|
||||
return files
|
||||
end
|
||||
|
||||
function M.get_project_files(directories)
|
||||
local conf = config.load()
|
||||
function M.get_project_files(directories, conf)
|
||||
local root = vim.fn.getcwd()
|
||||
local ignore_patterns = load_gitignore_patterns(root)
|
||||
local ignore_patterns = load_gitignore_patterns(root, conf)
|
||||
local all_files = {}
|
||||
for _, dir in ipairs(directories) do
|
||||
local abs_dir = dir
|
||||
if not abs_dir:match("^/") then
|
||||
abs_dir = root .. "/" .. dir
|
||||
end
|
||||
scandir(abs_dir, ignore_patterns, all_files)
|
||||
scandir(abs_dir, ignore_patterns, all_files, conf)
|
||||
end
|
||||
|
||||
local rel_files = {}
|
||||
@@ -117,14 +153,13 @@ function M.get_project_files(directories)
|
||||
return rel_files
|
||||
end
|
||||
|
||||
function M.get_project_structure(directories)
|
||||
local files = M.get_project_files(directories)
|
||||
function M.get_project_structure(directories, conf)
|
||||
local files = M.get_project_files(directories, conf)
|
||||
local structure = "Files:\n" .. table.concat(files, "\n")
|
||||
return structure
|
||||
end
|
||||
|
||||
function M.get_file_contents(files)
|
||||
local conf = config.load()
|
||||
function M.get_file_contents(files, conf)
|
||||
local root = vim.fn.getcwd()
|
||||
local sections = {}
|
||||
for _, f in ipairs(files) do
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
local M = {}
|
||||
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 st = uv.fs_stat(path)
|
||||
@@ -16,8 +18,7 @@ local function ensure_dir(path)
|
||||
return true
|
||||
end
|
||||
|
||||
function M.get_clipboard_content()
|
||||
local conf = config.load()
|
||||
function M.get_clipboard_content(conf)
|
||||
local content = vim.fn.getreg('+')
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Clipboard content length: " .. #content .. "\n")
|
||||
@@ -25,8 +26,7 @@ function M.get_clipboard_content()
|
||||
return content
|
||||
end
|
||||
|
||||
function M.write_file(filepath, content)
|
||||
local conf = config.load()
|
||||
function M.write_file(filepath, content, conf)
|
||||
local dir = filepath:match("(.*)/")
|
||||
if dir and dir ~= "" then
|
||||
ensure_dir(dir)
|
||||
@@ -46,8 +46,7 @@ function M.write_file(filepath, content)
|
||||
end
|
||||
end
|
||||
|
||||
function M.delete_file(filepath)
|
||||
local conf = config.load()
|
||||
function M.delete_file(filepath, conf)
|
||||
local st = uv.fs_stat(filepath)
|
||||
if st then
|
||||
local success, err = uv.fs_unlink(filepath)
|
||||
@@ -69,100 +68,6 @@ function M.delete_file(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
-- Applies a unified diff to the specified file.
|
||||
function M.apply_diff(filepath, diff_content)
|
||||
local conf = config.load()
|
||||
local tmp_original = vim.fn.tempname()
|
||||
local tmp_patch = vim.fn.tempname()
|
||||
|
||||
-- 1) Read original file (or empty if it doesn't exist)
|
||||
local fd_in = uv.fs_open(filepath, "r", 438)
|
||||
local original_data = ""
|
||||
if fd_in then
|
||||
local stat = uv.fs_fstat(fd_in)
|
||||
original_data = uv.fs_read(fd_in, stat.size, 0)
|
||||
uv.fs_close(fd_in)
|
||||
end
|
||||
|
||||
-- 2) Write original content to a temp file
|
||||
local fd_orig = uv.fs_open(tmp_original, "w", 438)
|
||||
if fd_orig then
|
||||
uv.fs_write(fd_orig, original_data, -1)
|
||||
uv.fs_close(fd_orig)
|
||||
end
|
||||
|
||||
-- 3) Write diff to a temp file
|
||||
local fd_patch = uv.fs_open(tmp_patch, "w", 438)
|
||||
if fd_patch then
|
||||
uv.fs_write(fd_patch, diff_content, -1)
|
||||
uv.fs_close(fd_patch)
|
||||
else
|
||||
return false, "Could not open temporary file to write patch."
|
||||
end
|
||||
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Applying diff to: " .. filepath .. "\n")
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Original file contents saved at: " .. tmp_original .. "\n")
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch file saved at: " .. tmp_patch .. "\n")
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch contents:\n" .. diff_content .. "\n")
|
||||
end
|
||||
|
||||
-- 4) Attempt to run 'patch'
|
||||
local patch_cmd = "patch -u --reject-file=- " .. vim.fn.shellescape(tmp_original)
|
||||
.. " < " .. vim.fn.shellescape(tmp_patch)
|
||||
local handle = io.popen(patch_cmd)
|
||||
if not handle then
|
||||
return false, "Failed to run patch command."
|
||||
end
|
||||
local result = handle:read("*a")
|
||||
local success_close, errmsg = handle:close()
|
||||
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command was: " .. patch_cmd .. "\n")
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command output:\n" .. (result or "") .. "\n")
|
||||
if not success_close then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command closed with error: " .. (errmsg or "unknown") .. "\n")
|
||||
else
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command succeeded.\n")
|
||||
end
|
||||
end
|
||||
|
||||
if not success_close then
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command failed: " .. (errmsg or "unknown") .. "\n")
|
||||
end
|
||||
return false, errmsg
|
||||
end
|
||||
|
||||
-- If successful, read the patched file and write it back
|
||||
if conf.debug then
|
||||
local debug_read = uv.fs_open(tmp_original, "r", 438)
|
||||
if debug_read then
|
||||
local debug_stat = uv.fs_fstat(debug_read)
|
||||
local debug_data = uv.fs_read(debug_read, debug_stat.size, 0)
|
||||
uv.fs_close(debug_read)
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Post-patch temp file:\n" .. debug_data .. "\n")
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] End of patched temp file content.\n")
|
||||
end
|
||||
end
|
||||
|
||||
local fd_out = uv.fs_open(tmp_original, "r", 438)
|
||||
if fd_out then
|
||||
local stat_out = uv.fs_fstat(fd_out)
|
||||
local new_data = uv.fs_read(fd_out, stat_out.size, 0)
|
||||
uv.fs_close(fd_out)
|
||||
|
||||
M.write_file(filepath, new_data)
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Successfully applied patch to: " .. filepath .. "\n")
|
||||
end
|
||||
else
|
||||
return false, "Could not read patched file."
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function M.finish()
|
||||
print("Finished processing files.")
|
||||
end
|
||||
|
||||
@@ -4,15 +4,21 @@ local context = require('chatgpt_nvim.context')
|
||||
local handler = require('chatgpt_nvim.handler')
|
||||
local config = require('chatgpt_nvim.config')
|
||||
local ui = require('chatgpt_nvim.ui')
|
||||
local prompts = require('chatgpt_nvim.prompts')
|
||||
|
||||
local tools_manager = require("chatgpt_nvim.tools.manager")
|
||||
local tools_module = require("chatgpt_nvim.tools")
|
||||
|
||||
local ok_yaml, lyaml = pcall(require, "lyaml")
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- UTILITIES
|
||||
------------------------------------------------------------------------------
|
||||
local function copy_to_clipboard(text)
|
||||
vim.fn.setreg('+', text)
|
||||
end
|
||||
|
||||
local function parse_response(raw)
|
||||
local conf = config.load()
|
||||
local function parse_response(raw, conf)
|
||||
if not ok_yaml then
|
||||
vim.api.nvim_err_writeln("lyaml not available. Install with `luarocks install lyaml`.")
|
||||
return nil
|
||||
@@ -48,49 +54,6 @@ local function read_file(path)
|
||||
return data
|
||||
end
|
||||
|
||||
local function is_directory(path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
return stat and stat.type == "directory"
|
||||
end
|
||||
|
||||
local function 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)
|
||||
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local name = vim.api.nvim_buf_get_name(b)
|
||||
@@ -100,259 +63,105 @@ local function close_existing_buffer_by_name(pattern)
|
||||
end
|
||||
end
|
||||
|
||||
local function preview_changes(changes)
|
||||
close_existing_buffer_by_name("ChatGPT_Changes_Preview$")
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Changes_Preview")
|
||||
vim.api.nvim_buf_set_option(bufnr, "filetype", "diff")
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||
"# Preview of Changes:",
|
||||
"# (Close this window to apply changes or use :q to cancel)",
|
||||
""
|
||||
})
|
||||
for _, fileinfo in ipairs(changes) do
|
||||
if fileinfo.delete then
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
|
||||
string.format("=== Delete file: %s ===", fileinfo.path or "<no path>"),
|
||||
""
|
||||
})
|
||||
elseif fileinfo.diff then
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
|
||||
string.format("=== Diff for file: %s ===", fileinfo.path or "<no path>")
|
||||
})
|
||||
local lines = vim.split(fileinfo.diff, "\n")
|
||||
for _, line in ipairs(lines) do
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line })
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
|
||||
elseif fileinfo.content then
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
|
||||
string.format("=== New file: %s ===", fileinfo.path or "<no path>")
|
||||
})
|
||||
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
|
||||
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
|
||||
end
|
||||
------------------------------------------------------------------------------
|
||||
-- PROMPT CONSTRUCTION
|
||||
------------------------------------------------------------------------------
|
||||
local function build_tools_available_block()
|
||||
local lines = {}
|
||||
lines[#lines+1] = "<tools_available>"
|
||||
for _, t in ipairs(tools_module.available_tools) do
|
||||
lines[#lines+1] = string.format("- **%s**: %s\n Usage: %s",
|
||||
t.name, t.explanation, t.usage
|
||||
)
|
||||
end
|
||||
|
||||
vim.cmd("vsplit")
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
lines[#lines+1] = "</tools_available>"
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function partial_accept(changes)
|
||||
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 function build_prompt(user_input, dirs, conf)
|
||||
local root = vim.fn.getcwd()
|
||||
local initial_files = conf.initial_files or {}
|
||||
local final_sections = {}
|
||||
|
||||
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
|
||||
if fileinfo.delete then
|
||||
table.insert(lines, string.format("[DELETE] %s", fileinfo.path or "<no path>"))
|
||||
elseif fileinfo.diff then
|
||||
table.insert(lines, string.format("[DIFF] %s", fileinfo.path or "<no path>"))
|
||||
local diff_lines = vim.split(fileinfo.diff, "\n")
|
||||
for _, dl in ipairs(diff_lines) do
|
||||
table.insert(lines, " " .. dl)
|
||||
end
|
||||
elseif fileinfo.content then
|
||||
table.insert(lines, string.format("[WRITE] %s", fileinfo.path or "<no path>"))
|
||||
local content_lines = vim.split(fileinfo.content, "\n")
|
||||
for _, cl in ipairs(content_lines) do
|
||||
table.insert(lines, " " .. cl)
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
-- 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
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
-- 2) <tools_available>
|
||||
table.insert(final_sections, build_tools_available_block())
|
||||
|
||||
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, delete = false, diff = nil, content = nil }
|
||||
local accum = {}
|
||||
-- 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"))
|
||||
|
||||
for _, line in ipairs(edited_lines) do
|
||||
if line:match("^#") or line == "" then
|
||||
goto continue
|
||||
-- 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
|
||||
|
||||
local del_match = line:match("^%[DELETE%] (.+)")
|
||||
local diff_match = line:match("^%[DIFF%] (.+)")
|
||||
local write_match = line:match("^%[WRITE%] (.+)")
|
||||
|
||||
if del_match or diff_match or write_match then
|
||||
-- store previous if any
|
||||
if keep_current and (current_fileinfo.path ~= nil) then
|
||||
if current_fileinfo.diff then
|
||||
current_fileinfo.diff = table.concat(accum, "\n")
|
||||
elseif current_fileinfo.content then
|
||||
current_fileinfo.content = table.concat(accum, "\n")
|
||||
end
|
||||
table.insert(final_changes, current_fileinfo)
|
||||
end
|
||||
|
||||
accum = {}
|
||||
keep_current = true
|
||||
|
||||
if del_match then
|
||||
current_fileinfo = { path = del_match, delete = true, diff = nil, content = nil }
|
||||
elseif diff_match then
|
||||
current_fileinfo = { path = diff_match, delete = false, diff = "", content = nil }
|
||||
elseif write_match then
|
||||
current_fileinfo = { path = write_match, delete = false, diff = nil, content = "" }
|
||||
end
|
||||
|
||||
else
|
||||
if keep_current then
|
||||
table.insert(accum, line:gsub("^%s*", ""))
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
if keep_current and (current_fileinfo.path ~= nil) then
|
||||
if current_fileinfo.diff ~= nil then
|
||||
current_fileinfo.diff = table.concat(accum, "\n")
|
||||
elseif current_fileinfo.content ~= nil then
|
||||
current_fileinfo.content = table.concat(accum, "\n")
|
||||
end
|
||||
table.insert(final_changes, current_fileinfo)
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, "modified", false)
|
||||
end
|
||||
if #file_content_blocks > 0 then
|
||||
table.insert(final_sections, table.concat(file_content_blocks, "\n\n"))
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = bufnr,
|
||||
once = true,
|
||||
callback = function()
|
||||
on_write()
|
||||
vim.cmd("bd! " .. bufnr)
|
||||
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"))
|
||||
|
||||
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
|
||||
return table.concat(final_sections, "\n\n")
|
||||
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)
|
||||
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
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
vim.cmd("vsplit")
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
return { prompts["step-prompt"] }
|
||||
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()
|
||||
------------------------------------------------------------------------------
|
||||
-- :ChatGPT
|
||||
------------------------------------------------------------------------------
|
||||
local function run_chatgpt_command()
|
||||
local conf = config.load()
|
||||
ui.setup_ui(conf)
|
||||
ui.debug_log("Running :ChatGPT command.")
|
||||
local dirs = conf.directories or {"."}
|
||||
if conf.interactive_file_selection then
|
||||
dirs = ui.pick_directories(dirs)
|
||||
dirs = ui.pick_directories(dirs, conf)
|
||||
if #dirs == 0 then
|
||||
dirs = conf.directories
|
||||
end
|
||||
@@ -367,7 +176,7 @@ function M.run_chatgpt_command()
|
||||
vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
|
||||
|
||||
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."
|
||||
})
|
||||
@@ -377,72 +186,24 @@ function M.run_chatgpt_command()
|
||||
callback = function()
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local user_input = table.concat(lines, "\n")
|
||||
if user_input == "" or user_input:find("^# Enter your 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_buf_set_option(bufnr, "modified", false)
|
||||
return
|
||||
end
|
||||
|
||||
local project_structure = context.get_project_structure(dirs)
|
||||
local initial_files = conf.initial_files or {}
|
||||
local included_sections = {}
|
||||
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||
|
||||
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})
|
||||
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
|
||||
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
|
||||
local bufnr_ref = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
|
||||
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
|
||||
|
||||
local 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, [[
|
||||
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"
|
||||
```
|
||||
|
||||
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)
|
||||
local prompt_lines = vim.split(chunks[1], "\n")
|
||||
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
|
||||
vim.cmd("vsplit")
|
||||
vim.cmd("buffer " .. bufnr_ref)
|
||||
|
||||
copy_to_clipboard(chunks[1])
|
||||
if #chunks == 1 then
|
||||
@@ -458,34 +219,41 @@ function M.run_chatgpt_command()
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
end
|
||||
|
||||
function M.run_chatgpt_paste_command()
|
||||
------------------------------------------------------------------------------
|
||||
-- :ChatGPTPaste
|
||||
------------------------------------------------------------------------------
|
||||
local function run_chatgpt_paste_command()
|
||||
local conf = config.load()
|
||||
ui.setup_ui(conf)
|
||||
ui.debug_log("Running :ChatGPTPaste command.")
|
||||
print("Reading ChatGPT YAML response from clipboard...")
|
||||
local raw = handler.get_clipboard_content()
|
||||
local raw = handler.get_clipboard_content(conf)
|
||||
if raw == "" then
|
||||
vim.api.nvim_err_writeln("Clipboard is empty. Please copy the YAML response from ChatGPT first.")
|
||||
return
|
||||
end
|
||||
|
||||
local data = parse_response(raw)
|
||||
local data = parse_response(raw, conf)
|
||||
if not data then
|
||||
return
|
||||
end
|
||||
|
||||
if data.commands and conf.enable_debug_commands then
|
||||
local results = {}
|
||||
for _, cmd in ipairs(data.commands) do
|
||||
table.insert(results, execute_debug_command(cmd))
|
||||
-- Check if we have tools
|
||||
if data.tools then
|
||||
-- Must also verify project name
|
||||
if not data.project_name or data.project_name ~= conf.project_name then
|
||||
vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
|
||||
return
|
||||
end
|
||||
local output = table.concat(results, "\n\n")
|
||||
copy_to_clipboard(output)
|
||||
print("Debug command results copied to clipboard!")
|
||||
|
||||
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
|
||||
ui.debug_log("Received project_name and files in response.")
|
||||
if data.project_name ~= conf.project_name then
|
||||
vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
|
||||
return
|
||||
@@ -493,7 +261,7 @@ function M.run_chatgpt_paste_command()
|
||||
|
||||
local is_final = false
|
||||
for _, fileinfo in ipairs(data.files) do
|
||||
if fileinfo.content or fileinfo.delete == true or fileinfo.diff then
|
||||
if fileinfo.content or fileinfo.delete == true then
|
||||
is_final = true
|
||||
break
|
||||
end
|
||||
@@ -501,7 +269,7 @@ function M.run_chatgpt_paste_command()
|
||||
|
||||
if is_final 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.")
|
||||
local closed = vim.wait(60000, function()
|
||||
local bufs = vim.api.nvim_list_bufs()
|
||||
@@ -521,7 +289,7 @@ function M.run_chatgpt_paste_command()
|
||||
|
||||
local final_files = data.files
|
||||
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
|
||||
vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.")
|
||||
return
|
||||
@@ -532,38 +300,28 @@ function M.run_chatgpt_paste_command()
|
||||
for _, fileinfo in ipairs(final_files) do
|
||||
if not fileinfo.path then
|
||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.")
|
||||
goto continue
|
||||
end
|
||||
|
||||
if not is_subpath(root, fileinfo.path) then
|
||||
vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path)
|
||||
goto continue
|
||||
end
|
||||
|
||||
if fileinfo.delete == true then
|
||||
ui.debug_log("Deleting file: " .. fileinfo.path)
|
||||
handler.delete_file(fileinfo.path)
|
||||
print("Deleted: " .. fileinfo.path)
|
||||
elseif fileinfo.diff then
|
||||
ui.debug_log("Applying diff to file: " .. fileinfo.path)
|
||||
local success, err = handler.apply_diff(fileinfo.path, fileinfo.diff)
|
||||
if not success then
|
||||
vim.api.nvim_err_writeln("Error applying diff: " .. (err or "unknown"))
|
||||
else
|
||||
print("Patched: " .. fileinfo.path)
|
||||
end
|
||||
elseif fileinfo.content then
|
||||
ui.debug_log("Writing new file: " .. fileinfo.path)
|
||||
handler.write_file(fileinfo.path, fileinfo.content)
|
||||
print("Wrote: " .. fileinfo.path)
|
||||
else
|
||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'diff', 'content', or 'delete'.")
|
||||
if not is_subpath(root, fileinfo.path) then
|
||||
vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path)
|
||||
else
|
||||
if fileinfo.delete == true then
|
||||
ui.debug_log("Deleting file: " .. fileinfo.path)
|
||||
handler.delete_file(fileinfo.path, conf)
|
||||
print("Deleted: " .. fileinfo.path)
|
||||
elseif fileinfo.content then
|
||||
ui.debug_log("Writing file: " .. fileinfo.path)
|
||||
handler.write_file(fileinfo.path, fileinfo.content, conf)
|
||||
print("Wrote: " .. fileinfo.path)
|
||||
else
|
||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.")
|
||||
end
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
else
|
||||
-- Not final => user is requesting more files
|
||||
local requested_paths = {}
|
||||
local root = vim.fn.getcwd()
|
||||
for _, fileinfo in ipairs(data.files) do
|
||||
if fileinfo.path then
|
||||
table.insert(requested_paths, fileinfo.path)
|
||||
@@ -571,125 +329,76 @@ function M.run_chatgpt_paste_command()
|
||||
end
|
||||
|
||||
local file_sections = {}
|
||||
local root = vim.fn.getcwd()
|
||||
for _, f in ipairs(requested_paths) do
|
||||
local path = root .. "/" .. f
|
||||
local content = read_file(path)
|
||||
if content then
|
||||
table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n" .. content .. "\n```\n")
|
||||
table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content))
|
||||
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
|
||||
|
||||
local sections = {
|
||||
config.load().initial_prompt,
|
||||
conf.initial_prompt,
|
||||
"\n\nProject name: " .. (conf.project_name or ""),
|
||||
"\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n",
|
||||
table.concat(file_sections, "\n"),
|
||||
"\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (use `diff`, `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 estimate_fn = get_estimate_fn()
|
||||
local token_count = estimate_fn(prompt)
|
||||
ui.debug_log("Returning requested files. Token count: " .. token_count)
|
||||
local length = #prompt
|
||||
ui.debug_log("Returning requested files. Character count: " .. length)
|
||||
|
||||
if token_count > (conf.token_limit or 8000) and conf.enable_step_by_step then
|
||||
local step_message = [[
|
||||
It appears this requested data is quite large. Please split the task into smaller steps
|
||||
and continue step by step.
|
||||
Which files would you need for the first step?
|
||||
]]
|
||||
copy_to_clipboard(step_message)
|
||||
if length > (conf.prompt_char_limit or 8000) and conf.enable_step_by_step then
|
||||
local large_step = prompts["step-prompt"]
|
||||
copy_to_clipboard(large_step)
|
||||
print("Step-by-step guidance copied to clipboard!")
|
||||
return
|
||||
elseif token_count > (conf.token_limit or 8000) then
|
||||
vim.api.nvim_err_writeln("Requested files exceed token limit. No step-by-step support enabled.")
|
||||
elseif length > (conf.prompt_char_limit or 8000) then
|
||||
vim.api.nvim_err_writeln("Requested files exceed prompt character limit. No step-by-step support enabled.")
|
||||
return
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
function M.run_chatgpt_current_buffer_command()
|
||||
------------------------------------------------------------------------------
|
||||
-- :ChatGPTCurrentBuffer
|
||||
------------------------------------------------------------------------------
|
||||
local function run_chatgpt_current_buffer_command()
|
||||
local conf = config.load()
|
||||
ui.setup_ui(conf)
|
||||
ui.debug_log("Running :ChatGPTCurrentBuffer command.")
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
local user_input = table.concat(lines, "\n")
|
||||
local dirs = conf.directories or {"."}
|
||||
if conf.interactive_file_selection then
|
||||
dirs = ui.pick_directories(dirs)
|
||||
dirs = ui.pick_directories(dirs, conf)
|
||||
if #dirs == 0 then
|
||||
dirs = conf.directories
|
||||
end
|
||||
end
|
||||
|
||||
local project_structure = context.get_project_structure(dirs)
|
||||
local initial_files = conf.initial_files or {}
|
||||
local included_sections = {}
|
||||
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||
|
||||
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})
|
||||
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
|
||||
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
|
||||
local bufnr_ref = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
|
||||
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
|
||||
|
||||
local 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, [[
|
||||
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"
|
||||
```
|
||||
|
||||
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)
|
||||
local prompt_lines = vim.split(chunks[1], "\n")
|
||||
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
|
||||
vim.cmd("vsplit")
|
||||
vim.cmd("buffer " .. bufnr_ref)
|
||||
|
||||
copy_to_clipboard(chunks[1])
|
||||
if #chunks == 1 then
|
||||
@@ -699,4 +408,11 @@ function M.run_chatgpt_current_buffer_command()
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- PUBLIC API
|
||||
------------------------------------------------------------------------------
|
||||
M.run_chatgpt_command = run_chatgpt_command
|
||||
M.run_chatgpt_paste_command = run_chatgpt_paste_command
|
||||
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
|
||||
|
||||
return M
|
||||
|
||||
367
lua/chatgpt_nvim/prompts.lua
Normal file
367
lua/chatgpt_nvim/prompts.lua
Normal 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 Solid’s 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 module’s 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 Go’s standard library where possible before adding third-party dependencies.
|
||||
- When introducing new functions or types, ensure they are uniquely named to avoid collisions.
|
||||
|
||||
5. **Import Management**
|
||||
- Ensure that every import is actually used in your code.
|
||||
- Remove unused imports to keep your code clean and maintainable.
|
||||
- Include all necessary imports for anything referenced in your code to avoid missing imports.
|
||||
- Verify that any introduced import paths match your module’s structure and do not cause naming conflicts.
|
||||
|
||||
6. **Output Format**
|
||||
- Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule.
|
||||
- When explaining your reasoning, include any relevant architectural trade-offs and rationale (e.g., “I placed function X in package Y to keep the domain-specific logic separate from the main execution flow.”).
|
||||
- If you modify an existing file, specify precisely which changes or additions you are making.
|
||||
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with Golang’s best practices for large, modular projects.
|
||||
]],
|
||||
["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 TYPO3’s recommended structure:
|
||||
- **Key files**: `ext_emconf.php`, `ext_localconf.php`, `ext_tables.php`, `ext_tables.sql`, `Configuration/`, `Classes/`, `Resources/`.
|
||||
- Use **PSR-4** autoloading and name extensions logically (e.g., `my_sitepackage`, `my_blogextension`).
|
||||
- Keep your extension’s code under `Classes/` (e.g., Controllers, Models, Services).
|
||||
- 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 site’s main configuration.”).
|
||||
- If you modify or extend an existing file, specify precisely which changes or additions you are making.
|
||||
|
||||
Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3’s best practices for large, maintainable projects.
|
||||
]],
|
||||
["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 Rust’s naming conventions (e.g., `snake_case` for files/modules, `PascalCase` for types).
|
||||
- Avoid duplicating the same function or type in multiple crates; share common functionality via a dedicated library crate if needed.
|
||||
|
||||
3. **Folder & Module Structure**
|
||||
- Organize code within each crate using Rust’s module system, keeping related functions and types in logical modules/submodules.
|
||||
- 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 root’s `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 component’s 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 you’ve 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
|
||||
37
lua/chatgpt_nvim/tools/edit_file.lua
Normal file
37
lua/chatgpt_nvim/tools/edit_file.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local handler = require("chatgpt_nvim.handler")
|
||||
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
||||
local path = tool_call.path
|
||||
local new_content = tool_call.content
|
||||
|
||||
if not path or not new_content then
|
||||
return "[edit_file] Missing 'path' or 'content'."
|
||||
end
|
||||
local root = vim.fn.getcwd()
|
||||
|
||||
if not is_subpath(root, path) then
|
||||
return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path)
|
||||
end
|
||||
|
||||
-- 1) Write the new content
|
||||
handler.write_file(path, new_content, conf)
|
||||
|
||||
local msg = {}
|
||||
msg[#msg+1] = string.format("Tool [edit_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path)
|
||||
msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n"
|
||||
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content)
|
||||
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
|
||||
|
||||
-- 2) If auto_lint => run robust LSP approach
|
||||
if conf.auto_lint then
|
||||
local diag_str = robust_lsp.lsp_check_file_content(path, new_content, 3000) -- 3s wait
|
||||
msg[#msg+1] = "\n" .. diag_str
|
||||
end
|
||||
|
||||
return table.concat(msg, "")
|
||||
end
|
||||
|
||||
return M
|
||||
17
lua/chatgpt_nvim/tools/execute_command.lua
Normal file
17
lua/chatgpt_nvim/tools/execute_command.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
local M = {}
|
||||
|
||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
||||
local cmd = tool_call.command
|
||||
if not cmd then
|
||||
return "[executeCommand] Missing 'command'."
|
||||
end
|
||||
local handle = io.popen(cmd)
|
||||
if not handle then
|
||||
return string.format("Tool [executeCommand '%s'] FAILED to popen.", cmd)
|
||||
end
|
||||
local result = handle:read("*a") or ""
|
||||
handle:close()
|
||||
return string.format("Tool [executeCommand '%s'] Result:\n%s", cmd, result)
|
||||
end
|
||||
|
||||
return M
|
||||
39
lua/chatgpt_nvim/tools/init.lua
Normal file
39
lua/chatgpt_nvim/tools/init.lua
Normal file
@@ -0,0 +1,39 @@
|
||||
local read_file_tool = require("chatgpt_nvim.tools.read_file")
|
||||
local edit_file_tool = require("chatgpt_nvim.tools.edit_file")
|
||||
local replace_in_file_tool = require("chatgpt_nvim.tools.replace_in_file")
|
||||
local execute_command_tool = require("chatgpt_nvim.tools.execute_command")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- We can store a table of available tools here
|
||||
M.available_tools = {
|
||||
{
|
||||
name = "readFile",
|
||||
usage = "Retrieve the contents of a file. Provide { tool='readFile', path='...' }",
|
||||
explanation = "Use this to read file content directly from the disk."
|
||||
},
|
||||
{
|
||||
name = "editFile",
|
||||
usage = "Overwrite an entire file's content. Provide { tool='editFile', path='...', content='...' }",
|
||||
explanation = "Use this when you want to replace a file with new content."
|
||||
},
|
||||
{
|
||||
name = "replace_in_file",
|
||||
usage = "Perform a search-and-replace. Provide { tool='replace_in_file', path='...', replacements=[ { search='...', replace='...' }, ... ] }",
|
||||
explanation = "Use this to apply incremental changes without fully overwriting the file."
|
||||
},
|
||||
{
|
||||
name = "executeCommand",
|
||||
usage = "Run a shell command. Provide { tool='executeCommand', command='...' }",
|
||||
explanation = "Use with caution, especially for destructive operations (rm, sudo, etc.)."
|
||||
},
|
||||
}
|
||||
|
||||
M.tools_by_name = {
|
||||
readFile = read_file_tool,
|
||||
editFile = edit_file_tool,
|
||||
replace_in_file = replace_in_file_tool,
|
||||
executeCommand = execute_command_tool
|
||||
}
|
||||
|
||||
return M
|
||||
177
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
177
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
local api = vim.api
|
||||
local lsp = vim.lsp
|
||||
|
||||
local M = {}
|
||||
|
||||
local function guess_filetype(path)
|
||||
local ext = path:match("^.+%.([^./\\]+)$")
|
||||
if not ext then return nil end
|
||||
ext = ext:lower()
|
||||
local map = {
|
||||
lua = "lua",
|
||||
go = "go",
|
||||
rs = "rust",
|
||||
js = "javascript",
|
||||
jsx = "javascriptreact",
|
||||
ts = "typescript",
|
||||
tsx = "typescriptreact",
|
||||
php = "php",
|
||||
}
|
||||
return map[ext]
|
||||
end
|
||||
|
||||
local function create_scratch_buffer(path, content)
|
||||
-- Create a unique buffer name so we never clash with an existing one
|
||||
local scratch_name = string.format("chatgpt-scratch://%s#%d", path, math.random(100000, 999999))
|
||||
|
||||
local bufnr = api.nvim_create_buf(false, true)
|
||||
if bufnr == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Assign the unique name to the buffer
|
||||
api.nvim_buf_set_name(bufnr, scratch_name)
|
||||
|
||||
-- Convert content string to lines
|
||||
local lines = {}
|
||||
for line in (content.."\n"):gmatch("(.-)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
|
||||
-- Mark it as a scratch buffer
|
||||
api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
|
||||
api.nvim_buf_set_option(bufnr, "swapfile", false)
|
||||
|
||||
return bufnr
|
||||
end
|
||||
|
||||
local function attach_existing_lsp_client(bufnr, filetype)
|
||||
local clients = lsp.get_active_clients()
|
||||
if not clients or #clients == 0 then
|
||||
return nil, "No active LSP clients"
|
||||
end
|
||||
|
||||
for _, client in ipairs(clients) do
|
||||
local ft_conf = client.config.filetypes or {}
|
||||
if vim.tbl_contains(ft_conf, filetype) then
|
||||
if not client.attached_buffers[bufnr] then
|
||||
vim.lsp.buf_attach_client(bufnr, client.id)
|
||||
end
|
||||
return client.id
|
||||
end
|
||||
end
|
||||
return nil, ("No active LSP client supports filetype '%s'"):format(filetype)
|
||||
end
|
||||
|
||||
local function send_did_open(bufnr, client_id, path, filetype)
|
||||
local client = lsp.get_client_by_id(client_id)
|
||||
if not client then
|
||||
return "Invalid client ID."
|
||||
end
|
||||
|
||||
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
|
||||
-- Even though the buffer name is unique, the LSP server sees this file as if at 'path'
|
||||
local uri = vim.uri_from_fname(path)
|
||||
|
||||
local didOpenParams = {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
languageId = filetype,
|
||||
version = 0,
|
||||
text = text,
|
||||
}
|
||||
}
|
||||
client.rpc.notify("textDocument/didOpen", didOpenParams)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function send_did_change(bufnr, client_id)
|
||||
local client = lsp.get_client_by_id(client_id)
|
||||
if not client then return end
|
||||
|
||||
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
local version = 1
|
||||
|
||||
client.rpc.notify("textDocument/didChange", {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
version = version,
|
||||
},
|
||||
contentChanges = {
|
||||
{ text = text }
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
local function wait_for_diagnostics(bufnr, timeout_ms)
|
||||
local done = false
|
||||
local result_diags = {}
|
||||
|
||||
local augrp = api.nvim_create_augroup("chatgpt_lsp_diag_"..bufnr, { clear = true })
|
||||
api.nvim_create_autocmd("DiagnosticChanged", {
|
||||
group = augrp,
|
||||
callback = function(args)
|
||||
if args.buf == bufnr then
|
||||
local diags = vim.diagnostic.get(bufnr)
|
||||
result_diags = diags
|
||||
done = true
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local waited = 0
|
||||
local interval = 50
|
||||
while not done and waited < timeout_ms do
|
||||
vim.cmd(("sleep %d m"):format(interval))
|
||||
waited = waited + interval
|
||||
end
|
||||
|
||||
pcall(api.nvim_del_augroup_by_id, augrp)
|
||||
return result_diags
|
||||
end
|
||||
|
||||
local function diagnostics_to_string(diags)
|
||||
if #diags == 0 then
|
||||
return "No LSP diagnostics reported."
|
||||
end
|
||||
local lines = { "--- LSP Diagnostics ---" }
|
||||
for _, d in ipairs(diags) do
|
||||
local sev = vim.diagnostic.severity[d.severity] or d.severity
|
||||
lines[#lines+1] = string.format("Line %d [%s]: %s", d.lnum + 1, sev, d.message)
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function M.lsp_check_file_content(path, new_content, timeout_ms)
|
||||
local filetype = guess_filetype(path) or "plaintext"
|
||||
|
||||
local bufnr = create_scratch_buffer(path, new_content)
|
||||
if not bufnr then
|
||||
return "(LSP) Could not create scratch buffer."
|
||||
end
|
||||
|
||||
local client_id, err = attach_existing_lsp_client(bufnr, filetype)
|
||||
if not client_id then
|
||||
api.nvim_buf_delete(bufnr, { force = true })
|
||||
return "(LSP) " .. (err or "No suitable LSP client.")
|
||||
end
|
||||
|
||||
local err2 = send_did_open(bufnr, client_id, path, filetype)
|
||||
if err2 then
|
||||
api.nvim_buf_delete(bufnr, { force = true })
|
||||
return "(LSP) " .. err2
|
||||
end
|
||||
|
||||
-- Optionally do a didChange
|
||||
send_did_change(bufnr, client_id)
|
||||
|
||||
local diags = wait_for_diagnostics(bufnr, timeout_ms or 2000)
|
||||
local diag_str = diagnostics_to_string(diags)
|
||||
|
||||
api.nvim_buf_delete(bufnr, { force = true })
|
||||
return diag_str
|
||||
end
|
||||
|
||||
return M
|
||||
79
lua/chatgpt_nvim/tools/manager.lua
Normal file
79
lua/chatgpt_nvim/tools/manager.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
local tools_module = require("chatgpt_nvim.tools")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function is_destructive_command(cmd)
|
||||
if not cmd then return false end
|
||||
local destructive_list = { "rm", "sudo", "mv", "cp" }
|
||||
for _, keyword in ipairs(destructive_list) do
|
||||
if cmd:match("(^" .. keyword .. "[%s$])") or cmd:match("[%s]" .. keyword .. "[%s$]") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function prompt_user_tool_accept(tool_call, conf)
|
||||
local auto_accept = conf.tool_auto_accept[tool_call.tool]
|
||||
|
||||
-- If this is an executeCommand and we see it's destructive, force a user prompt
|
||||
if tool_call.tool == "executeCommand" and auto_accept then
|
||||
if is_destructive_command(tool_call.command) then
|
||||
auto_accept = false
|
||||
end
|
||||
end
|
||||
|
||||
if auto_accept then
|
||||
-- If auto-accepted and not destructive, no prompt needed
|
||||
return true
|
||||
else
|
||||
-- Build some context about the tool request
|
||||
local msg = ("Tool request: %s\n"):format(tool_call.tool or "unknown")
|
||||
if tool_call.path then
|
||||
msg = msg .. ("Path: %s\n"):format(tool_call.path)
|
||||
end
|
||||
if tool_call.command then
|
||||
msg = msg .. ("Command: %s\n"):format(tool_call.command)
|
||||
end
|
||||
if tool_call.replacements then
|
||||
msg = msg .. ("Replacements: %s\n"):format(vim.inspect(tool_call.replacements))
|
||||
end
|
||||
|
||||
msg = msg .. "Accept this tool request? [y/N]: "
|
||||
|
||||
-- Force a screen redraw so the user sees the prompt properly
|
||||
vim.cmd("redraw")
|
||||
|
||||
local ans = vim.fn.input(msg)
|
||||
return ans:lower() == "y"
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
|
||||
local messages = {}
|
||||
|
||||
for _, call in ipairs(tools) do
|
||||
local accepted = prompt_user_tool_accept(call, conf)
|
||||
if not accepted then
|
||||
table.insert(messages, ("Tool [%s] was rejected by user."):format(call.tool or "nil"))
|
||||
else
|
||||
local tool_impl = tools_module.tools_by_name[call.tool]
|
||||
if tool_impl then
|
||||
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
|
||||
table.insert(messages, msg)
|
||||
else
|
||||
table.insert(messages, ("Unknown tool type: '%s'"):format(call.tool or "nil"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local combined = table.concat(messages, "\n\n")
|
||||
local limit = conf.prompt_char_limit or 8000
|
||||
if #combined > limit then
|
||||
return ("The combined tool output is too large (%d chars). Please break down the operations into smaller steps."):format(#combined)
|
||||
end
|
||||
return combined
|
||||
end
|
||||
|
||||
M.handle_tool_calls = handle_tool_calls
|
||||
return M
|
||||
17
lua/chatgpt_nvim/tools/read_file.lua
Normal file
17
lua/chatgpt_nvim/tools/read_file.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
local uv = vim.loop
|
||||
local M = {}
|
||||
|
||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
||||
local path = tool_call.path
|
||||
if not path then
|
||||
return "[read_file] Missing 'path'."
|
||||
end
|
||||
local file_data = read_file(path)
|
||||
if file_data then
|
||||
return string.format("Tool [read_file for '%s'] Result:\n\n%s", path, file_data)
|
||||
else
|
||||
return string.format("Tool [read_file for '%s'] FAILED. File not found or not readable.", path)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
51
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal file
51
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
local handler = require("chatgpt_nvim.handler")
|
||||
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function search_and_replace(original, replacements)
|
||||
local updated = original
|
||||
for _, r in ipairs(replacements) do
|
||||
local search_str = r.search or ""
|
||||
local replace_str = r.replace or ""
|
||||
updated = updated:gsub(search_str, replace_str)
|
||||
end
|
||||
return updated
|
||||
end
|
||||
|
||||
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
|
||||
local path = tool_call.path
|
||||
local replacements = tool_call.replacements or {}
|
||||
|
||||
if not path or #replacements == 0 then
|
||||
return "[replace_in_file] Missing 'path' or 'replacements'."
|
||||
end
|
||||
local root = vim.fn.getcwd()
|
||||
|
||||
if not is_subpath(root, path) then
|
||||
return string.format("Tool [replace_in_file for '%s'] REJECTED. Path outside project root.", path)
|
||||
end
|
||||
|
||||
local orig_data = read_file(path)
|
||||
if not orig_data then
|
||||
return string.format("[replace_in_file for '%s'] FAILED. Could not read file.", path)
|
||||
end
|
||||
|
||||
local updated_data = search_and_replace(orig_data, replacements)
|
||||
handler.write_file(path, updated_data, conf)
|
||||
|
||||
local msg = {}
|
||||
msg[#msg+1] = string.format("[replace_in_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path)
|
||||
msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n"
|
||||
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data)
|
||||
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
|
||||
|
||||
if conf.auto_lint then
|
||||
local diag_str = robust_lsp.lsp_check_file_content(path, updated_data, 3000)
|
||||
msg[#msg+1] = "\n" .. diag_str
|
||||
end
|
||||
|
||||
return table.concat(msg, "")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,40 +1,49 @@
|
||||
local M = {}
|
||||
local config = require('chatgpt_nvim.config')
|
||||
local conf = config.load()
|
||||
-- Remove 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
|
||||
if conf.improved_debug then
|
||||
-- Check if a debug buffer is already open. Close it first to avoid duplicates.
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name == "ChatGPT_Debug_Log" then
|
||||
vim.api.nvim_buf_delete(buf, {force = true})
|
||||
end
|
||||
end
|
||||
|
||||
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_option(debug_bufnr, "filetype", "log")
|
||||
-- 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
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name == "ChatGPT_Debug_Log" then
|
||||
vim.api.nvim_buf_delete(buf, {force = true})
|
||||
end
|
||||
end
|
||||
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_option(debug_bufnr, "filetype", "log")
|
||||
end
|
||||
end
|
||||
|
||||
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 })
|
||||
else
|
||||
if conf.debug then
|
||||
if runtime_conf and runtime_conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:debug] " .. msg .. "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.pick_directories(dirs)
|
||||
function M.pick_directories(dirs, conf)
|
||||
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
|
||||
table.insert(lines, d)
|
||||
end
|
||||
|
||||
-- If a file selection buffer is already open, close it to avoid confusion
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name:match("ChatGPT_File_Selection") then
|
||||
@@ -65,7 +74,6 @@ function M.pick_directories(dirs)
|
||||
once = true,
|
||||
callback = function()
|
||||
on_write()
|
||||
-- Automatically close the buffer once saved
|
||||
vim.cmd("bd! " .. bufnr)
|
||||
end
|
||||
})
|
||||
@@ -73,7 +81,6 @@ function M.pick_directories(dirs)
|
||||
vim.cmd("split")
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
|
||||
-- Wait up to 30s for user to close
|
||||
vim.wait(30000, function()
|
||||
local winids = vim.api.nvim_tabpage_list_wins(0)
|
||||
for _, w in ipairs(winids) do
|
||||
@@ -88,8 +95,4 @@ function M.pick_directories(dirs)
|
||||
return selected_dirs
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- The old chunkify method has been removed, since we now rely on step-by-step
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user