Compare commits

..

35 Commits

Author SHA1 Message Date
7fb8671840 fix: add ignore_files to config 2025-02-14 19:18:51 +01:00
32fcc2622f feat: remove old prompt_chat_limit 2025-02-14 15:26:28 +01:00
a5bad60d8e feat: dont include chatgpt_config.yaml in file list 2025-02-13 11:54:27 +01:00
1deae056b7 feat: change token calculation 2025-02-13 11:49:07 +01:00
07eceb4bee feat: change to token calculation again 2025-02-13 11:42:12 +01:00
216a4f2603 fix: init.lua 2025-02-13 01:57:02 +01:00
0617f5ba5b feat: rename execute_command tool, improve go prompt 2025-02-13 01:27:07 +01:00
51f7c2c66f feat: improve basic prompt 2025-02-13 01:18:42 +01:00
5b972c5c9f feat: add memory bank prompt, rename read_file and edit_file, improve basic prompt 2025-02-13 01:14:58 +01:00
01932be82a fix: missing char 2025-02-09 00:50:05 +01:00
e5f4558df5 feat: make prompt better to include real project name 2025-02-09 00:45:29 +01:00
0f044a625e fix: add function close_existing_buffer_by_name back 2025-02-09 00:14:45 +01:00
ebc0791542 feat: load configuration at each command, so it is not needed to restart nvim on config change 2025-02-09 00:08:10 +01:00
9d72ba46c4 feat: add file appending to prompt 2025-02-09 00:02:40 +01:00
2b9aa5f35f feat: adjust prompt 2025-02-08 23:50:50 +01:00
bf04d2f2fc feat: adjust basic prompt for better clarification 2025-02-08 23:48:00 +01:00
d9a1cf83fc feat: add ignore_files option 2025-02-08 03:16:18 +01:00
d19451ca5c refactor: prompt naming 2025-02-08 02:38:47 +01:00
4d679e43cb docs: update readme 2025-02-08 02:29:27 +01:00
d0712ef8a5 feat: add prompt for nodejs development 2025-02-08 02:29:20 +01:00
6723b802bf feat: add the possibility to also add all files to initial context 2025-02-08 02:21:12 +01:00
aa12bca3ab fix: hopefully this fixes lsp 2025-02-01 00:54:26 +01:00
e032be0118 fix: lsp should work now 2025-01-31 20:56:45 +01:00
deaec8094b fix: execute command tool, improving prompts 2025-01-31 18:22:57 +01:00
a27e3da769 fix: escaping in replace_in_file tool 2025-01-31 17:45:30 +01:00
84e71dcbef fix: improve the basic prompt 2025-01-31 17:28:20 +01:00
50ba937ae0 fix: add project name to initial prompt, improve basic prompt 2025-01-31 15:09:10 +01:00
d5b05ede36 fix: improve prompt for editFile command 2025-01-31 14:39:40 +01:00
5dc568e9e5 feat: also allow the config file without dot, give an error message at search replace if search was not found in file 2025-01-31 14:35:32 +01:00
e9fd204977 Merge pull request 'change-to-tools' (#2) from change-to-tools into main
Reviewed-on: #2
2025-01-31 13:38:29 +01:00
7aa00813cf feat: change the lsp buffer naming and improve prompt 2025-01-31 13:38:08 +01:00
58da08e26f feat: remove lint tool and add lsp integration 2025-01-31 11:13:58 +01:00
35bd0a7278 feat: add lint tool 2025-01-31 10:43:28 +01:00
0ff77954db feat: remove unneeded debug command approach 2025-01-31 10:31:09 +01:00
fd8df2abd5 initial change to tools logic 2025-01-31 09:49:15 +01:00
14 changed files with 1157 additions and 586 deletions

View File

@@ -1,17 +0,0 @@
project_name: "chatgpt_nvim"
default_prompt_blocks:
- "basic-prompt"
- "workflow-prompt"
directories:
- "."
initial_files:
- "README.md"
debug: false
preview_changes: false
interactive_file_selection: false
partial_acceptance: false
improved_debug: false
enable_debug_commands: true
token_limit: 128000
enable_chunking: false
enable_step_by_step: true

View File

@@ -4,28 +4,36 @@
This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to: This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to:
1. Generate prompts containing: 1. Generate prompts containing:
- An **initial prompt** (from `.chatgpt_config.yaml`) - An **initial prompt** (loaded from `.chatgpt_config.yaml` or `chatgpt_config.yaml`)
- A list of directories (also specified in `.chatgpt_config.yaml`) from which it gathers the project structure and file contents - A list of directories (as specified in your configuration) from which it gathers the project structure
- **Interactive file selection** if enabled, so you can pick exactly which directories to include - **Interactive file selection** (if enabled) so you can choose which directories to include
- Any **initial files** you define (e.g., `README.md`, etc.) - Any **initial files** you define (e.g. `README.md`, etc.)
- Optionally, **file contents** can be appended to the prompt when the new `include_file_contents` flag is enabled
2. Copy these prompts to your clipboard to paste into ChatGPT O1. 2. Copy these prompts to your clipboard to paste into ChatGPT O1.
3. Receive YAML changes from ChatGPT, then run `:ChatGPTPaste` to apply them or supply additional files. 3. Receive YAML changes from ChatGPT and run `:ChatGPTPaste` to apply them or to supply additional files.
## New Key Features ## New Key Features
- **Step-by-Step Prompting** (`enable_step_by_step: true`): - **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. This approach helps you stay within the models maximum token limit without having to manually break things apart. If the generated prompt grows too large (exceeding the configured `prompt_char_limit`), the plugin automatically produces a special prompt asking the model to split the task into smaller steps. This ensures you remain within the models maximum token capacity without manually breaking your task apart.
- **Partial Acceptance**: If `partial_acceptance: true`, you can open a buffer that lists the final changes. Remove or comment out lines you dont want, then only those changes are applied. - **Partial Acceptance**:
If `partial_acceptance: true`, you can open a buffer displaying the proposed changes. Remove or comment out lines you do not wish to apply, and only the remaining changes will be executed.
- **Preview Changes**: If `preview_changes: true`, you get a buffer showing proposed changes before you apply them. - **Preview Changes**:
If `preview_changes: true`, a buffer will show the proposed changes before they are applied.
- **Interactive File Selection**: If `interactive_file_selection: true`, you choose which directories from `.chatgpt_config.yaml` get included in the prompt, reducing token usage. - **Interactive File Selection**:
When `interactive_file_selection: true`, you can choose which directories (from your config file) to include in the prompt, thus managing token usage more effectively.
- **Improved Debug**: If `improved_debug: true`, debug logs go into a dedicated `ChatGPT_Debug_Log` buffer for easier reading. - **Include File Contents**:
A new configuration flag `include_file_contents` (default: `false`) lets you include the entire contents of the project files in the prompt. When enabled, the plugin gathers and appends the file contents from the selected directories. It counts the total characters and, if the combined file contents exceed the `prompt_char_limit`, it notifies you to disable this feature to avoid exceeding the models capacity.
## Example `.chatgpt_config.yaml` - **Improved Debug**:
If `improved_debug: true`, detailed debug logs are sent to a dedicated `ChatGPT_Debug_Log` buffer for easier review.
## Example `.chatgpt_config.yaml` or `chatgpt_config.yaml`
```yaml ```yaml
project_name: "chatgpt_nvim" project_name: "chatgpt_nvim"
@@ -37,40 +45,43 @@ directories:
initial_files: initial_files:
- "README.md" - "README.md"
debug: false debug: false
include_file_contents: false # Set to true to include file contents in the prompt
enable_step_by_step: true enable_step_by_step: true
preview_changes: true preview_changes: true
interactive_file_selection: true interactive_file_selection: true
partial_acceptance: true partial_acceptance: true
improved_debug: true improved_debug: true
token_limit: 3000 prompt_char_limit: 300000 # Maximum characters allowed in the prompt
``` ```
## Usage ## Usage
1. **`:ChatGPT`** 1. **`:ChatGPT`**
- If `interactive_file_selection` is on, youll pick directories to include in a buffer named `ChatGPT_File_Selection`. - If `interactive_file_selection` is enabled, you will select directories via a buffer (named `ChatGPT_File_Selection`).
- Save & close with `:wq`, `:x`, or `:bd` (you dont have to use `:q`). - Save and close the buffer with `:wq`, `:x`, or `:bd` (no need 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. - If `enable_step_by_step` is active and the prompt might exceed `prompt_char_limit`, the plugin will generate a step-by-step instruction prompt.
2. **Paste Prompt to ChatGPT** 2. **Paste Prompt to ChatGPT**
- If the task is split into steps, simply copy/paste them one by one into ChatGPT. - If the task is split into steps, copy and paste them one by one into ChatGPT.
3. **`:ChatGPTPaste`** 3. **`:ChatGPTPaste`**
- The plugin reads the YAML from your clipboard. If it requests more files, it might again suggest a step-by-step approach. - The plugin reads the YAML response from your clipboard. If additional files are requested, a step-by-step approach might be suggested.
- If final changes are provided: - When final changes are provided:
- Optionally preview them (`preview_changes`). - You can optionally preview them (`preview_changes`).
- Optionally partially accept them (`partial_acceptance`). - You can optionally partially accept them (`partial_acceptance`).
- Then the plugin writes/deletes files as specified. - Finally, the plugin applies file writes or deletions as specified in the YAML.
## Troubleshooting & Tips ## Troubleshooting & Tips
- Adjust `token_limit` in `.chatgpt_config.yaml` as needed.
- If partial acceptance is confusing, remember to remove or prepend `#` to lines you dont want before saving and closing the buffer. - Adjust `prompt_char_limit` in your configuration file as needed.
- If step-by-step prompting occurs, ensure you follow each prompt the model provides in the correct order. - If partial acceptance is confusing, simply remove or comment out lines you do not want before finalizing the buffer.
- Check `ChatGPT_Debug_Log` if `improved_debug` is on, or the Neovim messages if `debug` is on, for detailed info. - When step-by-step prompting occurs, ensure you follow each instruction in the correct order.
- You can close the selection or prompt buffers at any time with commands like `:bd`, `:x`, or `:wq`. No need to rely on `:q`. - For detailed debug information, check the `ChatGPT_Debug_Log` buffer (if `improved_debug` is enabled) or the standard Neovim messages (if `debug` is enabled).
- You can close the selection or prompt buffers at any time using commands like `:bd`, `:x`, or `:wq`.
## Debug Commands ## Debug Commands
If `enable_debug_commands` is true, you can include commands like these in your YAML:
If `enable_debug_commands` is true, you can include commands such as:
```yaml ```yaml
commands: commands:
- command: "list" - command: "list"
@@ -80,6 +91,23 @@ commands:
pattern: "searchString" pattern: "searchString"
target: "path/to/file/or/directory" target: "path/to/file/or/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. The **list** command uses the Linux `ls` command to list directory contents, while the **grep** command searches for a given pattern within files or directories.
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support! Enjoy your improved, flexible ChatGPT NeoVim plugin with step-by-step support and enhanced file inclusion capabilities!
## Test when Developing
- **Add 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
```

29
chatgpt_config.yaml Normal file
View File

@@ -0,0 +1,29 @@
project_name: "chatgpt.vim"
default_prompt_blocks:
- "basic"
- "secure-coding"
ignore_files:
- "node_modules/"
- "*.log"
- "vendor/"
include_file_contents: true
debug: false
improved_debug: false
preview_changes: false
interactive_file_selection: false
partial_acceptance: false
enable_debug_commands: true
enable_chunking: false
enable_step_by_step: true
auto_lint: true
tool_auto_accept:
read_file: true
edit_file: true
replace_in_file: true
execute_command: false

View File

@@ -4,6 +4,7 @@ local uv = vim.loop
local ok_yaml, lyaml = pcall(require, "lyaml") local ok_yaml, lyaml = pcall(require, "lyaml")
local prompts = require("chatgpt_nvim.prompts") local prompts = require("chatgpt_nvim.prompts")
-- Determines the Git root or fallback directory.
local function get_project_root() local function get_project_root()
local current_file = vim.fn.expand("%:p") local current_file = vim.fn.expand("%:p")
local root_dir local root_dir
@@ -28,37 +29,74 @@ local function get_project_root()
return root_dir return root_dir
end end
-- Attempt to locate either .chatgpt_config.yaml or chatgpt_config.yaml, returning whichever exists first.
local function get_config_path() local function get_config_path()
local root = get_project_root() local root = get_project_root()
return root .. "/.chatgpt_config.yaml" local dot_config = root .. "/.chatgpt_config.yaml"
local non_dot_config = root .. "/chatgpt_config.yaml"
local dot_fd = uv.fs_open(dot_config, "r", 438)
if dot_fd then
uv.fs_close(dot_fd)
return dot_config
end
local non_dot_fd = uv.fs_open(non_dot_config, "r", 438)
if non_dot_fd then
uv.fs_close(non_dot_fd)
return non_dot_config
end
return nil
end end
function M.load() function M.load()
-- Attempt to find either config file, else fallback
local path = get_config_path() local path = get_config_path()
local fd = uv.fs_open(path, "r", 438)
local config = { local config = {
initial_prompt = "", initial_prompt = "",
directories = { "." }, directories = { "." },
default_prompt_blocks = {}, default_prompt_blocks = {},
-- Changed default from 128000 to 16384 as requested -- Removed prompt_char_limit
prompt_char_limit = 300000,
project_name = "", project_name = "",
debug = false, debug = false,
initial_files = {}, initial_files = {},
include_file_contents = false, -- NEW FLAG: include file contents in prompt if true
preview_changes = false, preview_changes = false,
interactive_file_selection = false, interactive_file_selection = false,
partial_acceptance = false, partial_acceptance = false,
improved_debug = false, improved_debug = false,
enable_chunking = false, enable_chunking = false,
-- New default for step-by-step
enable_step_by_step = true, enable_step_by_step = true,
enable_debug_commands = false
-- If auto_lint is true, we only do LSP-based checks.
auto_lint = false,
tool_auto_accept = {
read_file = false,
edit_file = false,
replace_in_file = false,
execute_command = false,
}
} }
-- If no config file found, alert user and return default config
if not path then
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
vim.notify(
"No config file found (tried .chatgpt_config.yaml, chatgpt_config.yaml). Using defaults.",
vim.log.levels.WARN
)
config.max_token = 2048
return config
end
local fd = uv.fs_open(path, "r", 438)
if fd then if fd then
local stat = uv.fs_fstat(fd) local stat = uv.fs_fstat(fd)
local data = uv.fs_read(fd, stat.size, 0) local data = uv.fs_read(fd, stat.size, 0)
uv.fs_close(fd) uv.fs_close(fd)
if data and ok_yaml then if data and ok_yaml then
local ok, result = pcall(lyaml.load, data) local ok, result = pcall(lyaml.load, data)
if ok and type(result) == "table" then if ok and type(result) == "table" then
@@ -71,9 +109,6 @@ function M.load()
if type(result.default_prompt_blocks) == "table" then if type(result.default_prompt_blocks) == "table" then
config.default_prompt_blocks = result.default_prompt_blocks config.default_prompt_blocks = result.default_prompt_blocks
end end
if type(result.prompt_char_limit) == "number" then
config.prompt_char_limit = result.prompt_char_limit
end
if type(result.project_name) == "string" then if type(result.project_name) == "string" then
config.project_name = result.project_name config.project_name = result.project_name
end end
@@ -83,6 +118,12 @@ function M.load()
if type(result.initial_files) == "table" then if type(result.initial_files) == "table" then
config.initial_files = result.initial_files config.initial_files = result.initial_files
end end
if type(result.include_file_contents) == "boolean" then
config.include_file_contents = result.include_file_contents
end
if type(result.ignore_files) == "table" then
config.ignore_files = result.ignore_files
end
if type(result.preview_changes) == "boolean" then if type(result.preview_changes) == "boolean" then
config.preview_changes = result.preview_changes config.preview_changes = result.preview_changes
end end
@@ -98,20 +139,35 @@ function M.load()
if type(result.enable_chunking) == "boolean" then if type(result.enable_chunking) == "boolean" then
config.enable_chunking = result.enable_chunking config.enable_chunking = result.enable_chunking
end end
-- Added logic to load enable_step_by_step from user config
if type(result.enable_step_by_step) == "boolean" then if type(result.enable_step_by_step) == "boolean" then
config.enable_step_by_step = result.enable_step_by_step config.enable_step_by_step = result.enable_step_by_step
end end
if type(result.enable_debug_commands) == "boolean" then
config.enable_debug_commands = result.enable_debug_commands 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
if type(result.max_token) == "number" then
config.max_token = result.max_token
else
config.max_token = 2048
end end
end end
end end
else else
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..." config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
config.max_token = 2048
end end
-- Merge the default prompt blocks with the config's initial prompt -- Merge default prompt blocks
if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
local merged_prompt = {} local merged_prompt = {}
for _, block_name in ipairs(config.default_prompt_blocks) do for _, block_name in ipairs(config.default_prompt_blocks) do
@@ -129,6 +185,12 @@ function M.load()
end end
end end
-- Include project name in the final initial prompt, if set
if config.project_name ~= "" then
config.initial_prompt =
"[Project Name: " .. config.project_name .. "]\n\n" .. config.initial_prompt
end
if config.debug then if config.debug then
vim.api.nvim_out_write("[chatgpt_nvim:config] Loaded config from: " .. path .. "\n") vim.api.nvim_out_write("[chatgpt_nvim:config] Loaded config from: " .. path .. "\n")
vim.api.nvim_out_write("[chatgpt_nvim:config] Debug logging is enabled.\n") vim.api.nvim_out_write("[chatgpt_nvim:config] Debug logging is enabled.\n")

View File

@@ -131,6 +131,11 @@ end
function M.get_project_files(directories, conf) function M.get_project_files(directories, conf)
local root = vim.fn.getcwd() local root = vim.fn.getcwd()
local ignore_patterns = load_gitignore_patterns(root, conf) local ignore_patterns = load_gitignore_patterns(root, conf)
if conf.ignore_files then
for _, pattern in ipairs(conf.ignore_files) do
table.insert(ignore_patterns, gitignore_to_lua_pattern(pattern))
end
end
local all_files = {} local all_files = {}
for _, dir in ipairs(directories) do for _, dir in ipairs(directories) do
local abs_dir = dir local abs_dir = dir
@@ -143,7 +148,9 @@ function M.get_project_files(directories, conf)
local rel_files = {} local rel_files = {}
for _, f in ipairs(all_files) do for _, f in ipairs(all_files) do
local rel = vim.fn.fnamemodify(f, ":.") local rel = vim.fn.fnamemodify(f, ":.")
table.insert(rel_files, rel) if not rel:match("^%.?chatgpt_config%.yaml$") then
table.insert(rel_files, rel)
end
end end
if conf.debug then if conf.debug then
@@ -185,4 +192,16 @@ function M.get_file_contents(files, conf)
return table.concat(sections, "\n") return table.concat(sections, "\n")
end end
return M -- NEW FUNCTION: Build the project prompt by optionally including file contents.
function M.get_project_prompt(directories, conf)
local structure = M.get_project_structure(directories, conf)
if conf.include_file_contents then
local files = M.get_project_files(directories, conf)
local contents = M.get_file_contents(files, conf)
return structure .. "\n" .. contents
else
return structure
end
end
return M

View File

@@ -6,8 +6,14 @@ local config = require('chatgpt_nvim.config')
local ui = require('chatgpt_nvim.ui') local ui = require('chatgpt_nvim.ui')
local prompts = require('chatgpt_nvim.prompts') local prompts = require('chatgpt_nvim.prompts')
local tools_manager = require("chatgpt_nvim.tools.manager")
local tools_module = require("chatgpt_nvim.tools")
local ok_yaml, lyaml = pcall(require, "lyaml") local ok_yaml, lyaml = pcall(require, "lyaml")
------------------------------------------------------------------------------
-- UTILITIES
------------------------------------------------------------------------------
local function copy_to_clipboard(text) local function copy_to_clipboard(text)
vim.fn.setreg('+', text) vim.fn.setreg('+', text)
end end
@@ -48,288 +54,153 @@ local function read_file(path)
return data return data
end end
local function is_directory(path) -- Added function to close existing buffers matching a name pattern.
local stat = vim.loop.fs_stat(path) local function close_existing_buffer_by_name(name_pattern)
return stat and stat.type == "directory" for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name:match(name_pattern) then
vim.api.nvim_buf_delete(buf, {force = true})
end
end
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
lines[#lines+1] = "</tools_available>"
return table.concat(lines, "\n")
end
local function build_prompt(user_input, dirs, conf)
local root = vim.fn.getcwd()
local initial_files = conf.initial_files or {}
local final_sections = {}
-- 1) <initial_prompts>
if conf.initial_prompt and conf.initial_prompt ~= "" then
table.insert(final_sections, "<initial_prompts>\n" .. conf.initial_prompt .. "\n</initial_prompts>\n")
end
-- 2) <tools_available>
table.insert(final_sections, build_tools_available_block())
-- 3) <task>
local task_lines = {}
task_lines[#task_lines+1] = "<task>"
task_lines[#task_lines+1] = user_input
for _, file_path in ipairs(initial_files) do
task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path)
end
task_lines[#task_lines+1] = "</task>\n"
table.insert(final_sections, table.concat(task_lines, "\n"))
-- 4) <file_content path="..."> from initial_files
local file_content_blocks = {}
for _, file_path in ipairs(initial_files) do
local full_path = root .. "/" .. file_path
if is_subpath(root, full_path) then
local fdata = read_file(full_path)
if fdata then
file_content_blocks[#file_content_blocks+1] = string.format(
"<file_content path=\"%s\">\n%s\n</file_content>", file_path, fdata
)
end
end
end
if #file_content_blocks > 0 then
table.insert(final_sections, table.concat(file_content_blocks, "\n\n"))
end
-- 4.1) Dynamic file inclusion via @ operator in user_input
local dynamic_files = {}
for file in user_input:gmatch("@([^%s]+)") do
if file ~= "chatgpt_config.yaml" and file ~= ".chatgpt_config.yaml" then
local already_included = false
for _, existing in ipairs(initial_files) do
if existing == file then
already_included = true
break
end
end
if not already_included then
table.insert(dynamic_files, file)
end
end
end
local dynamic_file_blocks = {}
for _, file in ipairs(dynamic_files) do
local full_path = root .. "/" .. file
if is_subpath(root, full_path) then
local fdata = read_file(full_path)
if fdata then
dynamic_file_blocks[#dynamic_file_blocks+1] = string.format(
"<file_content path=\"%s\">\n%s\n</file_content>", file, fdata
)
end
end
end
if #dynamic_file_blocks > 0 then
table.insert(final_sections, table.concat(dynamic_file_blocks, "\n\n"))
end
-- 5) <environment_details>
local env_lines = {}
env_lines[#env_lines+1] = "<environment_details>"
env_lines[#env_lines+1] = "# VSCode Visible Files"
for _, f in ipairs(initial_files) do
env_lines[#env_lines+1] = f
end
env_lines[#env_lines+1] = ""
env_lines[#env_lines+1] = "# VSCode Open Tabs"
env_lines[#env_lines+1] = "..."
env_lines[#env_lines+1] = ""
env_lines[#env_lines+1] = "# Current Time"
env_lines[#env_lines+1] = os.date("%x, %X (%Z)")
env_lines[#env_lines+1] = ""
env_lines[#env_lines+1] = "# Current Working Directory (" .. root .. ") Files"
env_lines[#env_lines+1] = context.get_project_prompt(dirs, conf) or ""
env_lines[#env_lines+1] = ""
env_lines[#env_lines+1] = "# Current Mode"
env_lines[#env_lines+1] = "ACT MODE"
env_lines[#env_lines+1] = "</environment_details>"
table.insert(final_sections, table.concat(env_lines, "\n"))
local final_prompt = table.concat(final_sections, "\n\n")
final_prompt = final_prompt:gsub("%chatgpt.vim%", conf.project_name)
return final_prompt
end
local function estimate_token_count(text)
local token_count = 0
for chunk in text:gmatch("%S+") do
for token in chunk:gmatch("(%w+|%p+)") do
token_count = token_count + 1
end
end
return token_count
end end
local function handle_step_by_step_if_needed(prompt, conf) local function handle_step_by_step_if_needed(prompt, conf)
local length = #prompt local token_count = estimate_token_count(prompt)
if not conf.enable_step_by_step or length <= (conf.prompt_char_limit or 8000) then local limit = conf.max_token or 2048
if (not conf.enable_step_by_step) or (token_count <= limit) then
return { prompt } return { prompt }
end end
return { prompts["step-prompt"] } return { prompts["step-prompt"] }
end 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)
if name:match(pattern) then
vim.api.nvim_buf_delete(b, { force = true })
end
end
end
local function preview_changes(changes, conf)
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
local indicator = (fileinfo.delete == true) and "Delete file" or "Write file"
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
string.format("=== %s: %s ===", indicator, fileinfo.path or "<no path>")
})
if fileinfo.content then
local lines = vim.split(fileinfo.content, "\n")
for _, line in ipairs(lines) do
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line })
end
end
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
end
vim.cmd("vsplit")
vim.cmd("buffer " .. bufnr)
end
local function partial_accept(changes, conf)
close_existing_buffer_by_name("ChatGPT_Partial_Accept$")
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Partial_Accept")
vim.api.nvim_buf_set_option(bufnr, "filetype", "diff")
local lines = {
"# Remove or comment out (prepend '#') any changes you do NOT want, then :wq, :x, or :bd to finalize partial acceptance",
""
}
for _, fileinfo in ipairs(changes) do
local action = (fileinfo.delete == true) and "[DELETE]" or "[WRITE]"
table.insert(lines, string.format("%s %s", action, fileinfo.path or "<no path>"))
if fileinfo.content then
local content_lines = vim.split(fileinfo.content, "\n")
for _, cl in ipairs(content_lines) do
table.insert(lines, " " .. cl)
end
end
table.insert(lines, "")
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local final_changes = {}
local function on_write()
local edited_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local keep_current = false
local current_fileinfo = { path = nil, content = nil, delete = false }
local content_accum = {}
for _, line in ipairs(edited_lines) do
if line:match("^#") or line == "" then
goto continue
end
local del_match = line:match("^%[DELETE%] (.+)")
local write_match = line:match("^%[WRITE%] (.+)")
if del_match then
if keep_current and (current_fileinfo.path ~= nil) then
if #content_accum > 0 then
current_fileinfo.content = table.concat(content_accum, "\n")
end
table.insert(final_changes, current_fileinfo)
end
keep_current = true
current_fileinfo = { path = del_match, delete = true, content = nil }
content_accum = {}
elseif write_match then
if keep_current and (current_fileinfo.path ~= nil) then
if #content_accum > 0 then
current_fileinfo.content = table.concat(content_accum, "\n")
end
table.insert(final_changes, current_fileinfo)
end
keep_current = true
current_fileinfo = { path = write_match, delete = false, content = nil }
content_accum = {}
else
if keep_current then
table.insert(content_accum, line:gsub("^%s*", ""))
end
end
::continue::
end
if keep_current and (current_fileinfo.path ~= nil) then
if #content_accum > 0 then
current_fileinfo.content = table.concat(content_accum, "\n")
end
table.insert(final_changes, current_fileinfo)
end
vim.api.nvim_buf_set_option(bufnr, "modified", false)
end
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = bufnr,
once = true,
callback = function()
on_write()
vim.cmd("bd! " .. bufnr)
end
})
vim.cmd("split")
vim.cmd("buffer " .. bufnr)
vim.wait(60000, function()
local winids = vim.api.nvim_tabpage_list_wins(0)
for _, w in ipairs(winids) do
local b = vim.api.nvim_win_get_buf(w)
if b == bufnr then
return false
end
end
return true
end)
return final_changes
end
local function store_prompt_for_reference(prompt, conf)
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Generated_Prompt")
vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown")
local lines = {
"# Below is the generated prompt. You can keep it for reference:",
""
}
local prompt_lines = vim.split(prompt, "\n")
for _, line in ipairs(prompt_lines) do
table.insert(lines, line)
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.cmd("vsplit")
vim.cmd("buffer " .. bufnr)
end
-- Updated to allow flags/args for 'ls' and 'grep'
local function execute_debug_command(cmd, conf)
if type(cmd) ~= "table" or not cmd.command then
return "Invalid command object."
end
local command = cmd.command
if command == "ls" then
-- Accept optional flags/args
local dir = cmd.dir or "."
local args = cmd.args or {}
local cmd_str = "ls"
if #args > 0 then
cmd_str = cmd_str .. " " .. table.concat(args, " ")
end
cmd_str = cmd_str .. " " .. dir
local handle = io.popen(cmd_str)
if not handle then
return "Failed to run ls command."
end
local result = handle:read("*a") or ""
handle:close()
return "Listing files in: " .. dir .. "\n" .. result
elseif command == "grep" then
-- Accept optional flags/args
-- If the user wants to do something like:
-- args: ["-r", "somePattern", "someDir"]
-- we just pass them all to grep.
local args = cmd.args or {}
if #args == 0 then
-- fallback to old usage
local pattern = cmd.pattern
local target = cmd.target
if not pattern or not target then
return "Usage for grep: {command='grep', args=['-r','pattern','target']} or {pattern='<text>', target='<path>'}"
end
local stat = vim.loop.fs_stat(target)
if not stat then
return "Cannot grep: target path does not exist"
end
-- old logic remains for backward compatibility
if stat.type == "directory" then
local handle = io.popen("ls -p " .. target .. " | grep -v /")
if not handle then
return "Failed to read directory contents for grep."
end
local all_files = {}
for file in handle:read("*a"):gmatch("[^\n]+") do
table.insert(all_files, target .. "/" .. file)
end
handle:close()
local results = {}
local function grep_in_file(search_string, filepath)
local content = read_file(filepath)
if not content then
return "Could not read file: " .. filepath
end
local lines = {}
local line_num = 0
for line in content:gmatch("([^\n]*)\n?") do
line_num = line_num + 1
if line:find(search_string, 1, true) then
table.insert(lines, filepath .. ":" .. line_num .. ":" .. line)
end
end
return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n")
end
for _, f in ipairs(all_files) do
local fstat = vim.loop.fs_stat(f)
if fstat and fstat.type == "file" then
table.insert(results, grep_in_file(pattern, f))
end
end
return table.concat(results, "\n")
else
local function grep_in_file(search_string, filepath)
local content = read_file(filepath)
if not content then
return "Could not read file: " .. filepath
end
local lines = {}
local line_num = 0
for line in content:gmatch("([^\n]*)\n?") do
line_num = line_num + 1
if line:find(search_string, 1, true) then
table.insert(lines, filepath .. ":" .. line_num .. ":" .. line)
end
end
return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n")
end
return grep_in_file(pattern, target)
end
else
-- new approach with flags/args
local cmd_str = "grep " .. table.concat(args, " ")
local handle = io.popen(cmd_str)
if not handle then
return "Failed to run grep command."
end
local result = handle:read("*a") or ""
handle:close()
return result
end
else
return "Unknown command: " .. command
end
end
local function run_chatgpt_command() local function run_chatgpt_command()
package.loaded["chatgpt_nvim.config"] = nil
local config = require("chatgpt_nvim.config")
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) ui.setup_ui(conf)
ui.debug_log("Running :ChatGPT command.") ui.debug_log("Running :ChatGPT command.")
@@ -345,13 +216,15 @@ local function run_chatgpt_command()
local bufnr = vim.api.nvim_create_buf(false, false) local bufnr = vim.api.nvim_create_buf(false, false)
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Prompt.md") vim.api.nvim_buf_set_name(bufnr, "ChatGPT_Prompt.md")
vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown") vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown")
vim.api.nvim_buf_set_option(bufnr, "omnifunc", "v:lua.chatgpt_file_complete")
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(bufnr, "buftype", "") vim.api.nvim_buf_set_option(bufnr, "buftype", "")
vim.api.nvim_buf_set_option(bufnr, "modifiable", true) vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"# Enter your prompt below.", "# Enter your main user prompt (task) below.",
"", "",
"You can include files by typing @filename in your prompt.",
"Save & close with :wq, :x, or :bd to finalize your prompt." "Save & close with :wq, :x, or :bd to finalize your prompt."
}) })
@@ -360,57 +233,25 @@ local function run_chatgpt_command()
callback = function() callback = function()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local user_input = table.concat(lines, "\n") local user_input = table.concat(lines, "\n")
if user_input == "" or user_input:find("^# Enter your prompt below.") then if user_input == "" or user_input:find("^# Enter your main user prompt %(task%) below.") then
vim.api.nvim_out_write("No valid input provided.\n") vim.api.nvim_out_write("No valid input provided.\n")
vim.api.nvim_buf_set_option(bufnr, "modified", false) vim.api.nvim_buf_set_option(bufnr, "modified", false)
return return
end end
local project_structure = context.get_project_structure(dirs, conf) local final_prompt = build_prompt(user_input, dirs, conf)
local initial_files = conf.initial_files or {} local chunks = handle_step_by_step_if_needed(final_prompt, conf)
local included_sections = {}
for _, item in ipairs(initial_files) do close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
local root = vim.fn.getcwd() local bufnr_ref = vim.api.nvim_create_buf(false, true)
local full_path = root .. "/" .. item vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
if is_directory(full_path) then vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
local dir_files = context.get_project_files({item}, conf)
for _, f in ipairs(dir_files) do
local path = root .. "/" .. f
local data = read_file(path)
if data then
table.insert(included_sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n")
end
end
else
local data = read_file(full_path)
if data then
table.insert(included_sections, "\nFile: `" .. item .. "`\n```\n" .. data .. "\n```\n")
end
end
end
local initial_sections = { local prompt_lines = vim.split(chunks[1], "\n")
"## Basic Prompt Instructions:\n", vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
conf.initial_prompt .. "\n\n\n", vim.cmd("vsplit")
"## User Instructions:\n", vim.cmd("buffer " .. bufnr_ref)
user_input .. "\n\n\n",
"## Context/Data:\n",
"Project name: " .. (conf.project_name or "") .. "\n",
"Project Structure:\n",
project_structure,
table.concat(included_sections, "\n")
}
if conf.enable_debug_commands then
table.insert(initial_sections, "\n## Debug Commands Info:\n")
table.insert(initial_sections, prompts["debug-commands-info"])
end
local prompt = table.concat(initial_sections, "\n")
store_prompt_for_reference(prompt, conf)
local chunks = handle_step_by_step_if_needed(prompt, conf)
copy_to_clipboard(chunks[1]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n") vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n")
@@ -426,6 +267,8 @@ local function run_chatgpt_command()
end end
local function run_chatgpt_paste_command() local function run_chatgpt_paste_command()
package.loaded["chatgpt_nvim.config"] = nil
local config = require("chatgpt_nvim.config")
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) ui.setup_ui(conf)
ui.debug_log("Running :ChatGPTPaste command.") ui.debug_log("Running :ChatGPTPaste command.")
@@ -441,19 +284,19 @@ local function run_chatgpt_paste_command()
return return
end end
if data.commands and conf.enable_debug_commands then if data.tools then
local results = {} if not data.project_name or data.project_name ~= conf.project_name then
for _, cmd in ipairs(data.commands) do vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
table.insert(results, execute_debug_command(cmd, conf)) return
end end
local output = table.concat(results, "\n\n")
copy_to_clipboard(output) local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file)
print("Debug command results copied to clipboard!") copy_to_clipboard(output_messages)
print("Tool call results have been processed and copied to clipboard.")
return return
end end
if data.project_name and data.files then if data.project_name and data.files then
ui.debug_log("Received project_name and files in response.")
if data.project_name ~= conf.project_name then if data.project_name ~= conf.project_name then
vim.api.nvim_err_writeln("Project name mismatch. Aborting.") vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
return return
@@ -469,7 +312,7 @@ local function run_chatgpt_paste_command()
if is_final then if is_final then
if conf.preview_changes then if conf.preview_changes then
preview_changes(data.files, conf) require('chatgpt_nvim.init').preview_changes(data.files, conf)
print("Close the preview window to apply changes, or use :q to cancel.") print("Close the preview window to apply changes, or use :q to cancel.")
local closed = vim.wait(60000, function() local closed = vim.wait(60000, function()
local bufs = vim.api.nvim_list_bufs() local bufs = vim.api.nvim_list_bufs()
@@ -489,7 +332,7 @@ local function run_chatgpt_paste_command()
local final_files = data.files local final_files = data.files
if conf.partial_acceptance then if conf.partial_acceptance then
final_files = partial_accept(data.files, conf) final_files = require('chatgpt_nvim.init').partial_accept(data.files, conf)
if #final_files == 0 then if #final_files == 0 then
vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.") vim.api.nvim_err_writeln("No changes remain after partial acceptance. Aborting.")
return return
@@ -500,30 +343,27 @@ local function run_chatgpt_paste_command()
for _, fileinfo in ipairs(final_files) do for _, fileinfo in ipairs(final_files) do
if not fileinfo.path then if not fileinfo.path then
vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.") vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.")
goto continue
end
if not is_subpath(root, fileinfo.path) then
vim.api.nvim_err_writeln("Invalid path outside project root: " .. fileinfo.path)
goto continue
end
if fileinfo.delete == true then
ui.debug_log("Deleting file: " .. fileinfo.path)
handler.delete_file(fileinfo.path, conf)
print("Deleted: " .. fileinfo.path)
elseif fileinfo.content then
ui.debug_log("Writing file: " .. fileinfo.path)
handler.write_file(fileinfo.path, fileinfo.content, conf)
print("Wrote: " .. fileinfo.path)
else else
vim.api.nvim_err_writeln("Invalid file entry. Must have '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 end
::continue::
end end
else else
local requested_paths = {} local requested_paths = {}
local root = vim.fn.getcwd()
for _, fileinfo in ipairs(data.files) do for _, fileinfo in ipairs(data.files) do
if fileinfo.path then if fileinfo.path then
table.insert(requested_paths, fileinfo.path) table.insert(requested_paths, fileinfo.path)
@@ -531,14 +371,13 @@ local function run_chatgpt_paste_command()
end end
local file_sections = {} local file_sections = {}
local root = vim.fn.getcwd()
for _, f in ipairs(requested_paths) do for _, f in ipairs(requested_paths) do
local path = root .. "/" .. f local path = root .. "/" .. f
local content = read_file(path) local content = read_file(path)
if content then if content then
table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n" .. content .. "\n```\n") table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content))
else else
table.insert(file_sections, "\nFile: `" .. f .. "`\n```\n(Could not read file)\n```\n") table.insert(file_sections, ("\nFile: `%s`\n```\n(Could not read file)\n```\n"):format(f))
end end
end end
@@ -547,35 +386,28 @@ local function run_chatgpt_paste_command()
"\n\nProject name: " .. (conf.project_name or ""), "\n\nProject name: " .. (conf.project_name or ""),
"\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n", "\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n",
table.concat(file_sections, "\n"), table.concat(file_sections, "\n"),
"\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (with `content` or `delete`) to apply changes.\n" "\n\nIf you need more files, please respond again in YAML listing additional files, or use the 'tools:' approach. If you have all info, provide final changes or continue your instructions."
} }
local prompt = table.concat(sections, "\n") local prompt = table.concat(sections, "\n")
local length = #prompt local length = #prompt
ui.debug_log("Returning requested files. Character count: " .. length) ui.debug_log("Returning requested files. Character count: " .. length)
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 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) copy_to_clipboard(prompt)
print("Prompt (with requested files) copied to clipboard! Paste it into the ChatGPT O1 model.") print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.")
end end
else else
vim.api.nvim_err_writeln("Invalid response. Expected 'project_name' and 'files'.") vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.")
end end
end end
local function run_chatgpt_current_buffer_command() local function run_chatgpt_current_buffer_command()
package.loaded["chatgpt_nvim.config"] = nil
local config = require("chatgpt_nvim.config")
local conf = config.load() local conf = config.load()
ui.setup_ui(conf) ui.setup_ui(conf)
ui.debug_log("Running :ChatGPTCurrentBuffer command.") ui.debug_log("Running :ChatGPTCurrentBuffer command.")
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local user_input = table.concat(lines, "\n") local user_input = table.concat(lines, "\n")
local dirs = conf.directories or {"."} local dirs = conf.directories or {"."}
@@ -586,92 +418,19 @@ local function run_chatgpt_current_buffer_command()
end end
end end
local project_structure = context.get_project_structure(dirs, conf) local final_prompt = build_prompt(user_input, dirs, conf)
local initial_files = conf.initial_files or {} local chunks = handle_step_by_step_if_needed(final_prompt, conf)
local included_sections = {}
local function is_directory(path) close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
local stat = vim.loop.fs_stat(path) local bufnr_ref = vim.api.nvim_create_buf(false, true)
return stat and stat.type == "directory" vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
end vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
local function read_file(path) local prompt_lines = vim.split(chunks[1], "\n")
local fd = vim.loop.fs_open(path, "r", 438) vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, prompt_lines)
if not fd then vim.cmd("vsplit")
return nil vim.cmd("buffer " .. bufnr_ref)
end
local stat = vim.loop.fs_fstat(fd)
if not stat then
vim.loop.fs_close(fd)
return nil
end
local data = vim.loop.fs_read(fd, stat.size, 0)
vim.loop.fs_close(fd)
return data
end
for _, item in ipairs(initial_files) do
local root = vim.fn.getcwd()
local full_path = root .. "/" .. item
if is_directory(full_path) then
local dir_files = context.get_project_files({item}, conf)
for _, f in ipairs(dir_files) do
local path = root .. "/" .. f
local data = read_file(path)
if data then
table.insert(included_sections, "\nFile: `" .. f .. "`\n```\n" .. data .. "\n```\n")
end
end
else
local data = read_file(full_path)
if data then
table.insert(included_sections, "\nFile: `" .. item .. "`\n```\n" .. data .. "\n```\n")
end
end
end
local initial_sections = {
"## Basic Prompt Instructions:\n",
conf.initial_prompt .. "\n\n\n",
"## User Instructions:\n",
user_input .. "\n\n\n",
"## Context/Data:\n",
"Project name: " .. (conf.project_name or "") .. "\n",
"Project Structure:\n",
project_structure,
table.concat(included_sections, "\n")
}
if conf.enable_debug_commands then
table.insert(initial_sections, "\n## Debug Commands Info:\n")
table.insert(initial_sections, prompts["debug-commands-info"])
end
local prompt = table.concat(initial_sections, "\n")
local function store_prompt_for_reference(pr)
close_existing_buffer_by_name("ChatGPT_Generated_Prompt$")
local bufnr_ref = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr_ref, "ChatGPT_Generated_Prompt")
vim.api.nvim_buf_set_option(bufnr_ref, "filetype", "markdown")
local lines_ref = {
"# Below is the generated prompt. You can keep it for reference:",
""
}
local pr_lines = vim.split(pr, "\n")
for _, line in ipairs(pr_lines) do
table.insert(lines_ref, line)
end
vim.api.nvim_buf_set_lines(bufnr_ref, 0, -1, false, lines_ref)
vim.cmd("vsplit")
vim.cmd("buffer " .. bufnr_ref)
end
store_prompt_for_reference(prompt, conf)
local chunks = handle_step_by_step_if_needed(prompt, conf)
copy_to_clipboard(chunks[1]) copy_to_clipboard(chunks[1])
if #chunks == 1 then if #chunks == 1 then
vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n") vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n")
@@ -683,6 +442,29 @@ end
M.run_chatgpt_command = run_chatgpt_command M.run_chatgpt_command = run_chatgpt_command
M.run_chatgpt_paste_command = run_chatgpt_paste_command M.run_chatgpt_paste_command = run_chatgpt_paste_command
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
M.execute_debug_command = execute_debug_command
function _G.chatgpt_file_complete(findstart, base)
if findstart == 1 then
local line = vim.fn.getline('.')
local col = vim.fn.col('.')
local start = line:sub(1, col):find("@[^%s]*$")
if start then
return start - 1
else
return -1
end
else
local conf = config.load()
local files = context.get_project_files({'.'}, conf)
local completions = {}
local esc_base = base:gsub("([^%w])", "%%%1")
for _, f in ipairs(files) do
if f:match("^" .. esc_base) then
table.insert(completions, f)
end
end
return completions
end
end
return M return M

View File

@@ -1,5 +1,82 @@
local M = { local M = {
["solidjs-development"] = [[ ["memory-bank"] = [[
# Memory Bank
You are Cline, an expert software engineer with a unique constraint: your memory periodically resets completely. This isn't a bug - it's what makes you maintain perfect documentation. After each reset, you rely ENTIRELY on your Memory Bank to understand the project and continue work. Without proper documentation, you cannot function effectively.
## Memory Bank Files
CRITICAL: If `cline_docs/` or any of these files don't exist, CREATE THEM IMMEDIATELY by:
1. Reading all provided documentation
2. Asking user for ANY missing information
3. Creating files with verified information only
4. Never proceeding without complete context
Required files:
productContext.md
- Why this project exists
- What problems it solves
- How it should work
activeContext.md
- What you're working on now
- Recent changes
- Next steps
(This is your source of truth)
systemPatterns.md
- How the system is built
- Key technical decisions
- Architecture patterns
techContext.md
- Technologies used
- Development setup
- Technical constraints
progress.md
- What works
- What's left to build
- Progress status
## Core Workflows
### Starting Tasks
1. Check for Memory Bank files
2. If ANY files missing, stop and create them
3. Read ALL files before proceeding
4. Verify you have complete context
5. Begin development. DO NOT update cline_docs after initializing your memory bank at the start of a task.
### During Development
1. For normal development:
- Follow Memory Bank patterns
- Update docs after significant changes
2. Say `[MEMORY BANK: ACTIVE]` at the beginning of every tool use.
### Memory Bank Updates
When user says "update memory bank":
1. This means imminent memory reset
2. Document EVERYTHING about current state
3. Make next steps crystal clear
4. Complete current task
Remember: After every memory reset, you begin completely fresh. Your only link to previous work is the Memory Bank. Maintain it as if your functionality depends on it - because it does.
]],
["solidjs"] = [[
### SolidJS Development Guidelines ### SolidJS Development Guidelines
You are helping me develop a large SolidJS application. Please keep the following points in mind when generating or explaining code: You are helping me develop a large SolidJS application. Please keep the following points in mind when generating or explaining code:
@@ -73,21 +150,111 @@ local M = {
Please follow these guidelines to ensure the generated or explained code aligns well with SolidJS best practices for large, maintainable projects. Please follow these guidelines to ensure the generated or explained code aligns well with SolidJS best practices for large, maintainable projects.
]], ]],
["go-development"] = [[ ["nodejs"] = [[
You are helping me develop a large Node.js application. Please keep the following points in mind when generating or explaining code:
1. **Project & Folder Structure**
- Maintain a clear, top-level directory layout. For example:
```
my-node-app/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── utils/
│ └── index.js (or app.js)
├── tests/
│ └── ...
├── package.json
├── .env (if needed)
├── .eslintrc.js
└── ...
```
- Keep your application logic separated in folders:
- **controllers/** (or handlers) for request handling logic,
- **services/** for core business logic or data processing,
- **models/** for database schemas/entities,
- **routes/** for defining routes/endpoints,
- **utils/** for reusable helper functions.
2. **Dependencies & Package Management**
- Use **npm** or **yarn** (pick one and stay consistent) to manage dependencies.
- Keep your `package.json` clean by removing unused dependencies.
- Pin exact versions for critical or sensitive dependencies to ensure reproducible builds.
3. **Configuration & Environment Variables**
- Use environment variables to store sensitive or environment-specific information (e.g., database credentials, API keys).
- Consider a config management library (like [dotenv](https://github.com/motdotla/dotenv) or [dotenv-flow](https://github.com/kerimdzhanov/dotenv-flow)) to load environment variables from `.env` files.
- Keep secrets out of version control (e.g., `.env` should be in `.gitignore`).
4. **Code Organization & Module Patterns**
- Use **ES Modules** (`import`/`export`) or **CommonJS** (`require`/`module.exports`) consistently across the project.
- Keep each module focused on a single responsibility.
- Use a **service layer** for business logic to avoid bloated controllers.
- Use **async/await** for asynchronous operations, ensuring proper error handling (try/catch blocks or .catch callbacks).
5. **Database & Data Persistence**
- Use an ORM/ODM (e.g., [Sequelize](https://sequelize.org/) for SQL, [Mongoose](https://mongoosejs.com/) for MongoDB) or a query builder ([Knex.js](https://knexjs.org/)) to maintain cleaner database interactions.
- Keep database-related logic in separate **models/** or **repositories/**.
- Handle database migrations carefully (e.g., with [db-migrate](https://db-migrate.readthedocs.io/), [Liquibase](https://www.liquibase.org/), or built-in ORM migration tools).
6. **Logging & Monitoring**
- Use a structured logging library (e.g., [Winston](https://github.com/winstonjs/winston), [Pino](https://github.com/pinojs/pino)) to capture logs in JSON or another parseable format.
- Ensure logs include enough context (request IDs, timestamps, etc.) to troubleshoot issues.
- Consider external logging and monitoring solutions (e.g., [Datadog](https://www.datadoghq.com/), [New Relic](https://newrelic.com/)) for production environments.
7. **Security & Best Practices**
- Sanitize and validate all user inputs (e.g., using libraries like [validator](https://github.com/validatorjs/validator.js)).
- Avoid SQL injection by using parameterized queries or ORM features.
- Implement rate limiting or request throttling to prevent abuse (e.g., [express-rate-limit](https://github.com/nfriedly/express-rate-limit)).
- Ensure **HTTPS** is used in production and secure headers are set (e.g., [helmet](https://github.com/helmetjs/helmet) for Express).
- Keep dependencies updated to patch known vulnerabilities (use `npm audit` or equivalent).
8. **Error Handling**
- Use centralized error handling middleware (if using a framework like Express) to catch and process errors consistently.
- Provide clear error messages but avoid leaking sensitive info in production.
- Separate operational errors (e.g., user-related) from programmer errors (e.g., logic bugs) to handle them appropriately.
9. **Testing & Quality Assurance**
- Write **unit tests** for individual modules or functions (e.g., using [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/)).
- Use **integration tests** or **end-to-end tests** (e.g., [Supertest](https://github.com/visionmedia/supertest) for API endpoints).
- Aim for high coverage but focus on critical business logic and error cases.
- Automate tests in your CI pipeline.
10. **Linting & Formatting**
- Use **ESLint** (with recommended or popular config like [Airbnb](https://www.npmjs.com/package/eslint-config-airbnb)) for consistent code quality.
- Use **Prettier** for code formatting to maintain a standardized style.
- Configure linting and formatting checks in a pre-commit hook or CI (e.g., [Husky](https://typicode.github.io/husky/), [lint-staged](https://github.com/okonet/lint-staged)).
11. **Deployment & Environment Management**
- Containerize your app with Docker if possible, specifying a secure and minimal base image.
- Use process managers like [PM2](https://pm2.keymetrics.io/) or systemd for production Node.js processes.
- Maintain separate configuration (or environment variables) for staging, production, etc.
12. **Output Format**
- Present any generated source code with clear folder and file placement (e.g., `controllers/`, `services/`).
- When explaining your reasoning, highlight the architectural decisions or patterns used (e.g., “I introduced a service layer to separate business logic from route handling.”).
- 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 Node.js best practices for large, maintainable projects.
]],
["go"] = [[
### Go Development Guidelines ### 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: 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** 1. **Go Modules & Dependency Management**
- Use a single `go.mod` file at the project root for module management. - Use a single `go.mod` file at the project root for module management.
- Ensure you use proper import paths based on the module name. - Ensure you use proper import paths based on the module name.
- If you refer to internal packages, use relative paths consistent with the modules structure (e.g., `moduleName/internal/packageA`). - If you refer to internal packages, use relative paths consistent with the modules structure (e.g., `moduleName/internal/packageA`).
- **Use the execute_command Tool for Dependencies:** Instead of manually editing version numbers in `go.mod`, please utilize the `execute_command` tool to run dependency commands (such as `go get`) to automatically fetch and update dependencies. This ensures that the correct versions are used without relying on manually provided values.
2. **Package Structure** 2. **Package Structure**
- Each folder should contain exactly one package. - Each folder should contain exactly one package.
- Avoid creating multiple packages in the same folder. - Avoid creating multiple packages in the same folder.
- Use descriptive folder names and keep package names concise, following Go naming conventions. - 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. - Do not duplicate function or type names across different files in the same folder/package.
3. **File & Folder Organization** 3. **File & Folder Organization**
- Organize source code in a folder hierarchy that reflects functionality. For example: - Organize source code in a folder hierarchy that reflects functionality. For example:
@@ -105,28 +272,26 @@ local M = {
└── shared/ └── shared/
``` ```
- Keep external-facing, reusable packages in `pkg/` and internal logic in `internal/`. - Keep external-facing, reusable packages in `pkg/` and internal logic in `internal/`.
- Place the `main()` function in the `cmd/<appname>` folder. - Place the `main()` function in the `cmd/<appname>` folder.
4. **Coding Best Practices** 4. **Coding Best Practices**
- Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols). - Maintain idiomatic Go code (e.g., short function and variable names where obvious, PascalCase for exported symbols).
- Keep functions short, focused, and tested. - Keep functions short, focused, and tested.
- Use Gos standard library where possible before adding third-party dependencies. - Use Gos standard library where possible before adding third-party dependencies.
- When introducing new functions or types, ensure they are uniquely named to avoid collisions. - When introducing new functions or types, ensure they are uniquely named to avoid collisions.
5. **Import Management** 5. **Import Management**
- Ensure that every import is actually used in your code. - Ensure that every import is actually used in your code.
- Remove unused imports to keep your code clean and maintainable. - Remove unused imports to keep your code clean and maintainable.
- Include all necessary imports for anything referenced in your code to avoid missing imports. - Include all necessary imports for anything referenced in your code to avoid missing imports.
- Verify that any introduced import paths match your modules structure and do not cause naming conflicts. - Verify that any introduced import paths match your modules structure and do not cause naming conflicts.
6. **Output Format** 6. **Output Format**
- Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule. - Present any generated source code as well-organized Go files, respecting the single-package-per-folder rule.
- When explaining your reasoning, include any relevant architectural trade-offs and rationale (e.g., “I placed function X in package Y to keep the domain-specific logic separate from the main execution flow.”). - When explaining your reasoning, include any relevant architectural trade-offs and rationale (e.g., “I placed function X in package `my_lib` to keep the business logic separate from the command-line interface.”).
- If you modify an existing file, specify precisely which changes or additions you are making. - If you modify existing files, specify precisely which changes or additions you are making.
Please follow these guidelines to ensure the generated or explained code aligns well with Golangs best practices for large, modular projects.
]], ]],
["typo3-development"] = [[ ["typo3"] = [[
### TYPO3 Development Guidelines ### TYPO3 Development Guidelines
You are helping me develop a large TYPO3 project. Please keep the following points in mind when generating or explaining code: You are helping me develop a large TYPO3 project. Please keep the following points in mind when generating or explaining code:
@@ -188,10 +353,8 @@ local M = {
- Clearly indicate where each file should be placed in the TYPO3 directory layout. - Clearly indicate where each file should be placed in the TYPO3 directory layout.
- When explaining your reasoning, include any relevant architectural decisions (e.g., “I created a separate extension for blog functionality to keep it isolated from the sites main configuration.”). - When explaining your reasoning, include any relevant architectural decisions (e.g., “I created a separate extension for blog functionality to keep it isolated from the sites main configuration.”).
- If you modify or extend an existing file, specify precisely which changes or additions you are making. - If you modify or extend an existing file, specify precisely which changes or additions you are making.
Please follow these guidelines to ensure the generated or explained code aligns well with TYPO3s best practices for large, maintainable projects.
]], ]],
["rust-development"] = [[ ["rust"] = [[
### Rust Development Guidelines ### Rust Development Guidelines
You are helping me develop a large Rust project. Please keep the following points in mind when generating or explaining code: You are helping me develop a large Rust project. Please keep the following points in mind when generating or explaining code:
@@ -231,14 +394,14 @@ local M = {
4. **Coding & Documentation Best Practices** 4. **Coding & Documentation Best Practices**
- Write **idiomatic Rust** code: - Write **idiomatic Rust** code:
- Use `cargo fmt` (formatting) and `cargo clippy` (linter) to maintain consistency and quality. - 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. - Use the `?` operator for error handling, preferring `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. - 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. - 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. - Keep functions short, focused, and ensure they have well-defined responsibilities.
5. **Reusability & Shared Code** 5. **Reusability & Shared Code**
- Place common or reusable functionality into a dedicated **library** crate. - 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`. - 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. - Use the Rust standard library whenever possible before introducing external dependencies.
6. **Error Handling & Logging** 6. **Error Handling & Logging**
@@ -250,60 +413,121 @@ local M = {
- Present generated source code as well-organized Rust files, respecting the single main library or binary per crate (`lib.rs` or `main.rs`). - 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.”). - 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. - 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"] = [[
### Basic Prompt ### Basic Prompt
1. **Analyze Required Files** You are assisting me in a coding workflow for a project (e.g., "%PROJECT_NAME%"). **Every time you inspect, modify, or execute operations on files, you must strictly follow the YAML format described below.** Under no circumstances should you output file operations in plain text or deviate from this structure.
- Check the project structure (or any provided file list).
- If you identify a file reference or function you do not have, **produce a YAML snippet** requesting that files content in full. For example: #### Mandatory Guidelines
1. **YAML-Only File Operations**
- **All operations must be provided within one single YAML block** that includes both the `project_name` and the `tools` array.
- If you need to ask questions or request clarifications, do so only in plain text separate from YAML. **Never include non-YAML tool commands.**
2. **Include the Project Name**
- Always include:
```yaml ```yaml
project_name: my_project project_name: "%PROJECT_NAME%"
files:
- path: "relative/path/to/needed_file"
``` ```
- Do not proceed with final changes if you lack the necessary file contents. This must be part of every YAML block you generate.
- Always provide the entire file content when you do include a file.
2. **Request File Contents** 3. **Operations Must Appear in the Tools Array**
- For every file you need but dont have, provide a short YAML snippet (like above) to retrieve it. - List all actions (e.g., reading, editing, replacing, executing commands) as items in the `tools:` array. If multiple actions are needed, include them sequentially within the same YAML block.
- Ask any clarifying questions outside the YAML code block.
3. **Provide Output YAML** 4. **Read Before Write Rule**
- When you have all information, output the final YAML in this format: - **Do not perform any write operations (using `edit_file` or `replace_in_file`) on an existing file unless you have already read its content in the current session using a `read_file` operation.**
- For new files (files that do not yet exist in the project), this rule does not apply.
- If yo already got the file contents in the first prompt, this rule does not apply.
- **Never** mix read_file with edit_file or replace_in_file in the same YAML block.
5. **File Inspection Before Modification**
- When you need to inspect a files contents, always use the following YAML format:
```yaml ```yaml
project_name: my_project project_name: "%PROJECT_NAME%"
tools:
- tool: "read_file"
path: "relative/path/to/file"
```
- Use the information from this operation to decide if and how to modify the file.
files: 6. **Modifying Files**
- path: "src/main.py" - To modify a file that you have already read, use:
```yaml
project_name: "%PROJECT_NAME%"
tools:
- tool: "edit_file"
path: "relative/path/to/file"
content: | content: |
# Updated main function # Full updated file content here
def main(): ```
print("Hello, World!") - Alternatively, for incremental changes, use:
```yaml
- path: "docs/README.md" project_name: "%PROJECT_NAME%"
delete: true tools:
- tool: "replace_in_file"
path: "relative/path/to/file"
replacements:
- search: "old text"
replace: "new text"
``` ```
- `project_name` must match the projects configured name.
- Each file item in `files` must have `path` and either `content` or `delete: true`.
4. **Explain Changes** 7. **Executing Commands**
- After the final YAML, add a brief explanation of the modifications (outside the YAML). - To run any shell command (e.g., testing, listing files), use:
```yaml
project_name: "%PROJECT_NAME%"
tools:
- tool: "execute_command"
command: "shell command here"
```
5. **Iterate as Needed** 8. **General Process**
- If further context or changes are required, repeat steps to request files or clarifications. - **Step 1: Gather Context / Ask Questions**
If any detail is unclear (such as file content or operation intent), ask your clarifying questions in plain text (not in YAML).
- **Step 2: Inspect Files**
Always use `read_file` to check file content before modifying.
- **Step 3: Repeat Steps 1 & 2 as Needed**
If further context is required, ask questions and read files again.
- **Step 4: Make Changes**
Only after reading the file, proceed to use `edit_file` or `replace_in_file`.
- **Step 5: Execute Commands if Needed**
Use `execute_command` as necessary, always within the YAML block.
- **Step 6: Tell that request is complete**
Once all operations are done, confirm that the request is complete with a little summary.
- **Step 7: Repeat other steps as necessary**
6. **Important Notes** #### Example YAML Block
- Never modify or delete a file you havent explicitly requested or received.
- Use comments in code only to clarify code logic, not to explain your thought process.
- Explanations go outside the YAML.
7. **Best Practices** ```yaml
- Keep file paths accurate to avoid mistakes during implementation. project_name: "%PROJECT_NAME%"
- Maintain a clear, logical structure for your changes. tools:
- Use the final YAML consistently for clarity. - tool: "read_file"
path: "relative/path/to/file"
- tool: "replace_in_file"
path: "relative/path/to/file"
replacements:
- search: "old text"
replace: "new text"
- tool: "edit_file"
path: "relative/path/to/file"
content: |
# Full updated file content here
- tool: "execute_command"
command: "ls -la"
```
#### Important Reminders
- **Always** include the `project_name` and the full YAML block with the `tools` array when calling operations.
- **Never** write or modify a file without first having read its content during the current session (unless the file is new).
- **Do not** produce any tool command output that is not strictly formatted as YAML.
---
This revised prompt ensures that during execution the placeholder "%PROJECT_NAME%" is replaced with the actual project name from the current configuration.
]], ]],
["secure-coding"] = [[ ["secure-coding"] = [[
### Secure Coding Guidelines ### Secure Coding Guidelines
@@ -370,33 +594,10 @@ local M = {
["step-prompt"] = [[ ["step-prompt"] = [[
It appears this request might exceed the model's prompt character limit if done all at once. It appears this request might exceed the model's prompt character limit if done all at once.
Please break down the tasks into smaller steps and handle them one by one. Please break down the tasks into smaller steps and handle them one by one.
At each step, we'll provide relevant files or context if needed.
Thank you! Thank you!
]], ]],
["file-selection-instructions"] = [[ ["file-selection-instructions"] = [[
Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd) Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd)
]],
["debug-commands-info"] = [[
### Debugging Commands
If you need debugging commands, include them in your YAML response as follows:
```yaml
commands:
- command: "ls"
args: ["-l", "path/to/directory"]
- command: "grep"
args: ["-r", "searchString", "path/to/file/or/directory"]
```
The "ls" command uses the system's 'ls' command to list directory contents. You can pass flags or additional arguments in `args`.
The "grep" command searches for a given pattern in files or directories, again receiving flags or additional arguments in `args`.
If you omit `args` for grep, you can still use the older format with `pattern` and `target` for backward compatibility.
This are the only two commands supported at the moment.
When these commands are present and `enable_debug_commands` is true, I'll execute them and return the results in the clipboard.
]] ]]
} }

View File

@@ -0,0 +1,37 @@
local handler = require("chatgpt_nvim.handler")
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
local M = {}
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
local path = tool_call.path
local new_content = tool_call.content
if not path or not new_content then
return "[edit_file] Missing 'path' or 'content'."
end
local root = vim.fn.getcwd()
if not is_subpath(root, path) then
return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path)
end
-- 1) Write the new content
handler.write_file(path, new_content, conf)
local msg = {}
msg[#msg+1] = string.format("Tool [edit_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path)
msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n"
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content)
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
-- 2) If auto_lint => run robust LSP approach
if conf.auto_lint then
local diag_str = robust_lsp.lsp_check_file_content(path, new_content, 3000) -- 3s wait
msg[#msg+1] = "\n" .. diag_str
end
return table.concat(msg, "")
end
return M

View File

@@ -0,0 +1,36 @@
local M = {}
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
-- Validate the command exists
local cmd = tool_call.command
if not cmd then
return "[execute_command] Missing 'command'."
end
-- Capture stderr and stdout together by redirecting stderr to stdout
-- This will help diagnose if there's an error causing no output
cmd = cmd .. " 2>&1"
-- Attempt to popen the command
local handle = io.popen(cmd, "r")
if not handle then
return string.format("Tool [execute_command '%s'] FAILED to popen.", cmd)
end
-- Read the full output (stdout + stderr)
local result = handle:read("*a") or ""
-- Attempt to close, capturing exit info
local _, exit_reason, exit_code = handle:close()
-- Provide a richer summary including exit code and reason
return string.format(
"Tool [execute_command '%s'] exited with code %s (%s)\n%s",
cmd,
tostring(exit_code),
tostring(exit_reason),
result
)
end
return M

View File

@@ -0,0 +1,39 @@
local read_file_tool = require("chatgpt_nvim.tools.read_file")
local edit_file_tool = require("chatgpt_nvim.tools.edit_file")
local replace_in_file_tool = require("chatgpt_nvim.tools.replace_in_file")
local execute_command_tool = require("chatgpt_nvim.tools.execute_command")
local M = {}
-- We can store a table of available tools here
M.available_tools = {
{
name = "read_file",
usage = "Retrieve the contents of a file. Provide { tool='read_file', path='...' }",
explanation = "Use this to read file content directly from the disk."
},
{
name = "edit_file",
usage = "Overwrite an entire file's content. Provide { tool='edit_file', path='...', content='...' }, Allways include the whole file 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 = "execute_command",
usage = "Run a shell command. Provide { tool='execute_command', command='...' }",
explanation = "Just run one single command per tool invocation, without comment. It must be a single line. Use with caution, especially for destructive operations (rm, sudo, etc.)."
},
}
M.tools_by_name = {
read_file = read_file_tool,
edit_file = edit_file_tool,
replace_in_file = replace_in_file_tool,
execute_command = execute_command_tool
}
return M

View File

@@ -0,0 +1,175 @@
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, path)
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_fname(path)
local version = 1
client.rpc.notify("textDocument/didChange", {
textDocument = {
uri = uri,
version = version,
},
contentChanges = {
{ text = text }
}
})
end
-- Use vim.wait to allow the event loop to process LSP diagnostics
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
})
vim.wait(timeout_ms, function()
return done
end, 50)
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 with consistent URI
send_did_change(bufnr, client_id, path)
local diags = wait_for_diagnostics(bufnr, timeout_ms or 2000)
local diag_str = diagnostics_to_string(diags)
api.nvim_buf_delete(bufnr, { force = true })
return diag_str
end
return M

View File

@@ -0,0 +1,79 @@
local tools_module = require("chatgpt_nvim.tools")
local M = {}
local function is_destructive_command(cmd)
if not cmd then return false end
local destructive_list = { "rm", "sudo", "mv", "cp" }
for _, keyword in ipairs(destructive_list) do
if cmd:match("(^" .. keyword .. "[%s$])") or cmd:match("[%s]" .. keyword .. "[%s$]") then
return true
end
end
return false
end
local function prompt_user_tool_accept(tool_call, conf)
local auto_accept = conf.tool_auto_accept[tool_call.tool]
-- If this is an execute_command and we see it's destructive, force a user prompt
if tool_call.tool == "execute_command" and auto_accept then
if is_destructive_command(tool_call.command) then
auto_accept = false
end
end
if auto_accept then
-- If auto-accepted and not destructive, no prompt needed
return true
else
-- Build some context about the tool request
local msg = ("Tool request: %s\n"):format(tool_call.tool or "unknown")
if tool_call.path then
msg = msg .. ("Path: %s\n"):format(tool_call.path)
end
if tool_call.command then
msg = msg .. ("Command: %s\n"):format(tool_call.command)
end
if tool_call.replacements then
msg = msg .. ("Replacements: %s\n"):format(vim.inspect(tool_call.replacements))
end
msg = msg .. "Accept this tool request? [y/N]: "
-- Force a screen redraw so the user sees the prompt properly
vim.cmd("redraw")
local ans = vim.fn.input(msg)
return ans:lower() == "y"
end
end
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
local messages = {}
for _, call in ipairs(tools) do
local accepted = prompt_user_tool_accept(call, conf)
if not accepted then
table.insert(messages, ("Tool [%s] was rejected by user."):format(call.tool or "nil"))
else
local tool_impl = tools_module.tools_by_name[call.tool]
if tool_impl then
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
table.insert(messages, msg)
else
table.insert(messages, ("Unknown tool type: '%s'"):format(call.tool or "nil"))
end
end
end
local combined = table.concat(messages, "\n\n")
local limit = conf.prompt_char_limit or 8000
if #combined > limit then
return ("The combined tool output is too large (%d chars). Please break down the operations into smaller steps."):format(#combined)
end
return combined
end
M.handle_tool_calls = handle_tool_calls
return M

View File

@@ -0,0 +1,17 @@
local uv = vim.loop
local M = {}
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
local path = tool_call.path
if not path then
return "[read_file] Missing 'path'."
end
local file_data = read_file(path)
if file_data then
return string.format("Tool [read_file for '%s'] Result:\n\n%s", path, file_data)
else
return string.format("Tool [read_file for '%s'] FAILED. File not found or not readable.", path)
end
end
return M

View File

@@ -0,0 +1,84 @@
local handler = require("chatgpt_nvim.handler")
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
local M = {}
-- Function to escape all Lua pattern magic characters:
local function escape_lua_pattern(s)
return s:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
end
local function search_and_replace(original, replacements)
local updated = original
local info_msgs = {}
for _, r in ipairs(replacements) do
local search_str = r.search or ""
local replace_str = r.replace or ""
-- Escape special pattern chars to ensure literal matching:
local escaped_search = escape_lua_pattern(search_str)
local replacement_count = 0
updated, replacement_count = updated:gsub(escaped_search, replace_str)
-- If the string was not found, append an info message
if replacement_count == 0 then
table.insert(info_msgs, string.format(
"[replace_in_file Info] The string '%s' was NOT found in the file and was not replaced.",
search_str
))
else
table.insert(info_msgs, string.format(
"[replace_in_file Info] The string '%s' was replaced %d time(s).",
search_str,
replacement_count
))
end
end
return updated, table.concat(info_msgs, "\n")
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
-- Now we get not only the updated data but also info messages about replacements
local updated_data, info_messages = 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 there are any info messages (strings not found, etc.), include them
if info_messages and info_messages ~= "" then
msg[#msg+1] = "\nAdditional info about the replacement operation:\n" .. info_messages .. "\n"
end
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