Compare commits
87 Commits
0d5ed2c52a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fb8671840 | |||
| 32fcc2622f | |||
| a5bad60d8e | |||
| 1deae056b7 | |||
| 07eceb4bee | |||
| 216a4f2603 | |||
| 0617f5ba5b | |||
| 51f7c2c66f | |||
| 5b972c5c9f | |||
| 01932be82a | |||
| e5f4558df5 | |||
| 0f044a625e | |||
| ebc0791542 | |||
| 9d72ba46c4 | |||
| 2b9aa5f35f | |||
| bf04d2f2fc | |||
| d9a1cf83fc | |||
| d19451ca5c | |||
| 4d679e43cb | |||
| d0712ef8a5 | |||
| 6723b802bf | |||
| aa12bca3ab | |||
| e032be0118 | |||
| deaec8094b | |||
| a27e3da769 | |||
| 84e71dcbef | |||
| 50ba937ae0 | |||
| d5b05ede36 | |||
| 5dc568e9e5 | |||
| e9fd204977 | |||
| 7aa00813cf | |||
| 58da08e26f | |||
| 35bd0a7278 | |||
| 0ff77954db | |||
| fd8df2abd5 | |||
| 59540981ed | |||
| e36ad99dcb | |||
| f7d986f5fc | |||
| b7274a310b | |||
| 904979b0f6 | |||
| b9643952cb | |||
| 1520641a04 | |||
| f1cc371294 | |||
| ca729140c3 | |||
| 6950c542ad | |||
| 3505a295a9 | |||
| 2c855e881b | |||
| 452253cdd0 | |||
| a77dbb683d | |||
| bae7d106ac | |||
| df206dce88 | |||
| 16d0e0645a | |||
| 0dc93c1d78 | |||
| 0dd3e6e6b6 | |||
| 3859f5531a | |||
| 381c5108d6 | |||
| 00e48586ad | |||
| 94a0bf059b | |||
| ff1a2e9f22 | |||
| 5333f9f9e0 | |||
| 9251043aec | |||
| 761ef836cd | |||
| 162ab2d820 | |||
| 40c9d5672d | |||
| a354ff080a | |||
| eb19c4144f | |||
| d6dc98cd58 | |||
| f0a5be81bd | |||
| b35860114d | |||
| 6a69c3edf3 | |||
| 48a5e5b7aa | |||
| 8ea5ffe91d | |||
| 37d9978314 | |||
| 71a3da5923 | |||
| 44a8458b97 | |||
| 63832ae0c3 | |||
| e97aa81d8f | |||
| 314a65a203 | |||
| f2c6f60d03 | |||
| cc37c8505c | |||
| 2eb79c2b1a | |||
| 00a52998b8 | |||
| 9a9868c991 | |||
| b2ecb15d7f | |||
| bd49a8903f | |||
| 6da204767c | |||
| eaa4ee2aa9 |
@@ -1,15 +0,0 @@
|
||||
initial_prompt: |
|
||||
You are a coding assistant who receives a project’s context and user instructions. The user will provide a prompt, and you will return modifications to the project in a YAML structure. This YAML must have a top-level key named files, which should be a list of file changes. Each element in files must be a mapping with the keys path and content. The path should be the file path relative to the project’s root directory, and content should contain the new file content as a multiline string (using the YAML | literal style). Do not include additional commentary outside of the YAML.
|
||||
Here is the structure expected in your final answer:
|
||||
files:
|
||||
- path: "relative/path/to/file1.ext"
|
||||
content: |
|
||||
<full file content here>
|
||||
- path: "relative/path/to/file2.ext"
|
||||
content: |
|
||||
<full file content here>
|
||||
Based on the prompt and project context provided, you must only return the YAML structure shown above (with actual file paths and contents substituted in). Any file that should be created or modified must appear as one of the files entries. The content should contain the complete and final code for that file, reflecting all changes requested by the user.
|
||||
|
||||
directories:
|
||||
- "lua"
|
||||
- "plugin"
|
||||
117
README.md
117
README.md
@@ -1,20 +1,113 @@
|
||||
<!-- README.md -->
|
||||
# ChatGPT NeoVim Plugin (Updated for YAML)
|
||||
# ChatGPT NeoVim Plugin (Extensively Updated with Step-by-Step Prompting)
|
||||
|
||||
This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to generate prompts containing:
|
||||
- An **initial prompt** configured via a `.chatgpt_config.yaml` file in your project root.
|
||||
- A list of directories (also specified in the `.chatgpt_config.yaml`) from which it gathers the complete project structure and file contents.
|
||||
- The current file and the project's `README.md`, if present.
|
||||
- It uses YAML for configuration and also expects the ChatGPT response in YAML.
|
||||
This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to:
|
||||
|
||||
It also respects `.gitignore` entries, skipping those files from the prompt.
|
||||
1. Generate prompts containing:
|
||||
- An **initial prompt** (loaded from `.chatgpt_config.yaml` or `chatgpt_config.yaml`)
|
||||
- A list of directories (as specified in your configuration) from which it gathers the project structure
|
||||
- **Interactive file selection** (if enabled) so you can choose which directories to include
|
||||
- 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
|
||||
|
||||
## Configuration
|
||||
2. Copy these prompts to your clipboard to paste into ChatGPT O1.
|
||||
3. Receive YAML changes from ChatGPT and run `:ChatGPTPaste` to apply them or to supply additional files.
|
||||
|
||||
Create a `.chatgpt_config.yaml` in your project root. For example:
|
||||
## New Key Features
|
||||
|
||||
- **Step-by-Step Prompting** (`enable_step_by_step: true`):
|
||||
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 model’s maximum token capacity without manually breaking your task apart.
|
||||
|
||||
- **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`, a buffer will show the proposed changes before they are applied.
|
||||
|
||||
- **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.
|
||||
|
||||
- **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 model’s capacity.
|
||||
|
||||
- **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
|
||||
initial_prompt: "You are a helpful coding assistant that follows instructions meticulously."
|
||||
project_name: "chatgpt_nvim"
|
||||
default_prompt_blocks:
|
||||
- "basic-prompt"
|
||||
- "workflow-prompt"
|
||||
directories:
|
||||
- "lua"
|
||||
- "plugin"
|
||||
- "."
|
||||
initial_files:
|
||||
- "README.md"
|
||||
debug: false
|
||||
include_file_contents: false # Set to true to include file contents in the prompt
|
||||
enable_step_by_step: true
|
||||
preview_changes: true
|
||||
interactive_file_selection: true
|
||||
partial_acceptance: true
|
||||
improved_debug: true
|
||||
prompt_char_limit: 300000 # Maximum characters allowed in the prompt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **`:ChatGPT`**
|
||||
- If `interactive_file_selection` is enabled, you will select directories via a buffer (named `ChatGPT_File_Selection`).
|
||||
- Save and close the buffer with `:wq`, `:x`, or `:bd` (no need to use `:q`).
|
||||
- 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**
|
||||
- If the task is split into steps, copy and paste them one by one into ChatGPT.
|
||||
|
||||
3. **`:ChatGPTPaste`**
|
||||
- The plugin reads the YAML response from your clipboard. If additional files are requested, a step-by-step approach might be suggested.
|
||||
- When final changes are provided:
|
||||
- You can optionally preview them (`preview_changes`).
|
||||
- You can optionally partially accept them (`partial_acceptance`).
|
||||
- Finally, the plugin applies file writes or deletions as specified in the YAML.
|
||||
|
||||
## Troubleshooting & Tips
|
||||
|
||||
- Adjust `prompt_char_limit` in your configuration file as needed.
|
||||
- If partial acceptance is confusing, simply remove or comment out lines you do not want before finalizing the buffer.
|
||||
- When step-by-step prompting occurs, ensure you follow each instruction in the correct order.
|
||||
- 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
|
||||
|
||||
If `enable_debug_commands` is true, you can include commands such as:
|
||||
```yaml
|
||||
commands:
|
||||
- command: "list"
|
||||
dir: "some/directory"
|
||||
|
||||
- command: "grep"
|
||||
pattern: "searchString"
|
||||
target: "path/to/file/or/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, 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
29
chatgpt_config.yaml
Normal 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
|
||||
@@ -1,37 +1,26 @@
|
||||
-- lua/chatgpt_nvim/config.lua
|
||||
-- Modified to:
|
||||
-- 1) Determine the Git root based on the currently opened file.
|
||||
-- 2) If no file is open or not in Git repo, fallback to current working directory.
|
||||
-- 3) Add support for configuring a list of default prompt blocks ("go-development", "typo3-development", "basic-prompt") that can override the initial prompt if provided.
|
||||
|
||||
local M = {}
|
||||
local uv = vim.loop
|
||||
|
||||
local ok_yaml, lyaml = pcall(require, "lyaml")
|
||||
local prompts = require("chatgpt_nvim.prompts")
|
||||
|
||||
-- Determines the Git root or fallback directory.
|
||||
local function get_project_root()
|
||||
-- Get the directory of the currently opened file.
|
||||
-- If no file is open, we fallback to current working directory.
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
local root_dir
|
||||
|
||||
if current_file == "" then
|
||||
-- No file opened, fallback to cwd
|
||||
root_dir = vim.fn.getcwd()
|
||||
else
|
||||
-- Extract directory from current file path
|
||||
local file_dir = current_file:match("(.*)/")
|
||||
if not file_dir then
|
||||
-- If something went wrong extracting the directory, fallback to cwd
|
||||
root_dir = vim.fn.getcwd()
|
||||
else
|
||||
-- Attempt to find git root from the file's directory
|
||||
local cmd = string.format("cd %s && git rev-parse --show-toplevel 2>/dev/null", vim.fn.shellescape(file_dir))
|
||||
local git_root = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error == 0 and git_root and #git_root > 0 then
|
||||
root_dir = git_root[1]
|
||||
else
|
||||
-- Not a git repo or failed to find git root, fallback to the file's directory
|
||||
root_dir = file_dir
|
||||
end
|
||||
end
|
||||
@@ -40,30 +29,74 @@ local function get_project_root()
|
||||
return root_dir
|
||||
end
|
||||
|
||||
-- Attempt to locate either .chatgpt_config.yaml or chatgpt_config.yaml, returning whichever exists first.
|
||||
local function get_config_path()
|
||||
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 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, and you must return the requested modifications or guidance. When returning modifications, follow the specified YAML structure. Keep your suggestions aligned with Go best practices and idiomatic Go.",
|
||||
["typo3-development"] = "You are a coding assistant specialized in TYPO3 development. You have access to the 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. When returning modifications, follow the YAML structure given.",
|
||||
["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 return modifications to the project in a YAML structure. The YAML must have a top-level key named files, which should be a list of file changes. Each element in files must be a mapping with the keys path and content. The path should be the file path relative to the project’s root directory, and content should contain the new file content as a multiline string. Do not include additional commentary outside of the YAML."
|
||||
}
|
||||
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
|
||||
|
||||
function M.load()
|
||||
-- Attempt to find either config file, else fallback
|
||||
local path = get_config_path()
|
||||
local fd = uv.fs_open(path, "r", 438)
|
||||
local config = {
|
||||
initial_prompt = "",
|
||||
directories = { "." },
|
||||
default_prompt_blocks = {}
|
||||
default_prompt_blocks = {},
|
||||
-- Removed prompt_char_limit
|
||||
project_name = "",
|
||||
debug = false,
|
||||
initial_files = {},
|
||||
include_file_contents = false, -- NEW FLAG: include file contents in prompt if true
|
||||
preview_changes = false,
|
||||
interactive_file_selection = false,
|
||||
partial_acceptance = false,
|
||||
improved_debug = false,
|
||||
enable_chunking = false,
|
||||
enable_step_by_step = true,
|
||||
|
||||
-- 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
|
||||
local stat = uv.fs_fstat(fd)
|
||||
local data = uv.fs_read(fd, stat.size, 0)
|
||||
uv.fs_close(fd)
|
||||
|
||||
if data and ok_yaml then
|
||||
local ok, result = pcall(lyaml.load, data)
|
||||
if ok and type(result) == "table" then
|
||||
@@ -76,25 +109,92 @@ function M.load()
|
||||
if type(result.default_prompt_blocks) == "table" then
|
||||
config.default_prompt_blocks = result.default_prompt_blocks
|
||||
end
|
||||
if type(result.project_name) == "string" then
|
||||
config.project_name = result.project_name
|
||||
end
|
||||
if type(result.debug) == "boolean" then
|
||||
config.debug = result.debug
|
||||
end
|
||||
if type(result.initial_files) == "table" then
|
||||
config.initial_files = result.initial_files
|
||||
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
|
||||
config.preview_changes = result.preview_changes
|
||||
end
|
||||
if type(result.interactive_file_selection) == "boolean" then
|
||||
config.interactive_file_selection = result.interactive_file_selection
|
||||
end
|
||||
if type(result.partial_acceptance) == "boolean" then
|
||||
config.partial_acceptance = result.partial_acceptance
|
||||
end
|
||||
if type(result.improved_debug) == "boolean" then
|
||||
config.improved_debug = result.improved_debug
|
||||
end
|
||||
if type(result.enable_chunking) == "boolean" then
|
||||
config.enable_chunking = result.enable_chunking
|
||||
end
|
||||
if type(result.enable_step_by_step) == "boolean" then
|
||||
config.enable_step_by_step = result.enable_step_by_step
|
||||
end
|
||||
|
||||
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
|
||||
else
|
||||
-- Default fallback configuration
|
||||
config.initial_prompt = "You are a coding assistant who receives a project’s context and user instructions. The user will provide a prompt, and you will return modifications to the project in a YAML structure. This YAML must have a top-level key named files, which should be a list of file changes. Each element in files must be a mapping with the keys path and content. The path should be the file path relative to the project’s root directory, and content should contain the new file content as a multiline string (using the YAML | literal style). Do not include additional commentary outside of the YAML.\n Here is the structure expected in your final answer:\n files:\n - path: \"relative/path/to/file1.ext\"\n content: |\n <full file content here>\n - path: \"relative/path/to/file2.ext\"\n content: |\n <full file content here>\n Based on the prompt and project context provided, you must only return the YAML structure shown above (with actual file paths and contents substituted in). Any file that should be created or modified must appear as one of the files entries. The content should contain the complete and final code for that file, reflecting all changes requested by the user."
|
||||
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
|
||||
config.max_token = 2048
|
||||
end
|
||||
|
||||
-- If default_prompt_blocks are specified, concatenate all matching prompts
|
||||
-- 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
|
||||
config.initial_prompt = table.concat(merged_prompt, "\n\n")
|
||||
local combined_blocks = table.concat(merged_prompt, "\n\n")
|
||||
if config.initial_prompt ~= "" then
|
||||
config.initial_prompt = config.initial_prompt .. "\n\n" .. combined_blocks
|
||||
else
|
||||
config.initial_prompt = combined_blocks
|
||||
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
|
||||
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")
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
@@ -2,10 +2,44 @@ local M = {}
|
||||
|
||||
local uv = vim.loop
|
||||
|
||||
local function load_gitignore_patterns(root)
|
||||
-- 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
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] No .gitignore found.\n")
|
||||
end
|
||||
return {}
|
||||
end
|
||||
local stat = uv.fs_fstat(fd)
|
||||
@@ -14,37 +48,78 @@ 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
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Loaded " .. #patterns .. " gitignore patterns.\n")
|
||||
end
|
||||
return patterns
|
||||
end
|
||||
|
||||
local function should_ignore_file(file, ignore_patterns)
|
||||
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
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function scandir(dir, ignore_patterns, files)
|
||||
files = files or {}
|
||||
local function is_text_file(file, conf)
|
||||
local fd = uv.fs_open(file, "r", 438)
|
||||
if not fd then
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open file: " .. file .. " for reading.\n")
|
||||
end
|
||||
return false
|
||||
end
|
||||
local chunk = uv.fs_read(fd, 1024, 0) or ""
|
||||
uv.fs_close(fd)
|
||||
if chunk:find("\0") then
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] File appears binary: " .. file .. "\n")
|
||||
end
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function scandir(dir, ignore_patterns, files, conf)
|
||||
local fd = uv.fs_opendir(dir, nil, 50)
|
||||
if not fd then return files end
|
||||
if not fd then
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open dir: " .. dir .. "\n")
|
||||
end
|
||||
return files
|
||||
end
|
||||
while true do
|
||||
local ents = uv.fs_readdir(fd)
|
||||
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" 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
|
||||
@@ -53,34 +128,45 @@ local function scandir(dir, ignore_patterns, files)
|
||||
return files
|
||||
end
|
||||
|
||||
function M.get_project_files(directories)
|
||||
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)
|
||||
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 = {}
|
||||
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 = {}
|
||||
for _, f in ipairs(all_files) do
|
||||
local rel = f:gsub("^" .. root .. "/", "")
|
||||
local rel = vim.fn.fnamemodify(f, ":.")
|
||||
if not rel:match("^%.?chatgpt_config%.yaml$") then
|
||||
table.insert(rel_files, rel)
|
||||
end
|
||||
end
|
||||
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Found " .. #rel_files .. " project files.\n")
|
||||
end
|
||||
|
||||
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)
|
||||
function M.get_file_contents(files, conf)
|
||||
local root = vim.fn.getcwd()
|
||||
local sections = {}
|
||||
for _, f in ipairs(files) do
|
||||
@@ -97,32 +183,25 @@ function M.get_file_contents(files)
|
||||
else
|
||||
uv.fs_close(fd)
|
||||
end
|
||||
else
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:context] Could not open file for content: " .. f .. "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(sections, "\n")
|
||||
end
|
||||
|
||||
function M.get_current_file()
|
||||
local current_path = vim.fn.expand("%:p")
|
||||
if current_path == "" then
|
||||
return nil, nil
|
||||
-- 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
|
||||
local fd = uv.fs_open(current_path, "r", 438)
|
||||
if not fd then return nil, current_path end
|
||||
local stat = uv.fs_fstat(fd)
|
||||
local data = uv.fs_read(fd, stat.size, 0)
|
||||
uv.fs_close(fd)
|
||||
return data, current_path
|
||||
end
|
||||
|
||||
function M.get_readme_content()
|
||||
local root = vim.fn.getcwd()
|
||||
local fd = uv.fs_open(root .. "/README.md", "r", 438)
|
||||
if not fd then return nil end
|
||||
local stat = uv.fs_fstat(fd)
|
||||
local data = uv.fs_read(fd, stat.size, 0)
|
||||
uv.fs_close(fd)
|
||||
return data
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,17 +1,71 @@
|
||||
local M = {}
|
||||
local uv = vim.loop
|
||||
|
||||
function M.get_clipboard_content()
|
||||
return vim.fn.getreg('+')
|
||||
-- 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)
|
||||
if st and st.type == 'directory' then
|
||||
return true
|
||||
end
|
||||
local parent = path:match("(.*)/")
|
||||
if parent and parent ~= "" then
|
||||
ensure_dir(parent)
|
||||
end
|
||||
uv.fs_mkdir(path, 511)
|
||||
return true
|
||||
end
|
||||
|
||||
function M.write_file(filepath, content)
|
||||
local fd = vim.loop.fs_open(filepath, "w", 438)
|
||||
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")
|
||||
end
|
||||
return content
|
||||
end
|
||||
|
||||
function M.write_file(filepath, content, conf)
|
||||
local dir = filepath:match("(.*)/")
|
||||
if dir and dir ~= "" then
|
||||
ensure_dir(dir)
|
||||
end
|
||||
local fd = uv.fs_open(filepath, "w", 438)
|
||||
if not fd then
|
||||
vim.api.nvim_err_writeln("Could not open file for writing: " .. filepath)
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Failed to open file for writing: " .. filepath .. "\n")
|
||||
end
|
||||
return
|
||||
end
|
||||
vim.loop.fs_write(fd, content, -1)
|
||||
vim.loop.fs_close(fd)
|
||||
uv.fs_write(fd, content, -1)
|
||||
uv.fs_close(fd)
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Successfully wrote file: " .. filepath .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
function M.delete_file(filepath, conf)
|
||||
local st = uv.fs_stat(filepath)
|
||||
if st then
|
||||
local success, err = uv.fs_unlink(filepath)
|
||||
if not success then
|
||||
vim.api.nvim_err_writeln("Could not delete file: " .. filepath .. " - " .. (err or "unknown error"))
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Failed to delete file: " .. filepath .. "\n")
|
||||
end
|
||||
else
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] Deleted file: " .. filepath .. "\n")
|
||||
end
|
||||
end
|
||||
else
|
||||
vim.api.nvim_err_writeln("File not found, cannot delete: " .. filepath)
|
||||
if conf.debug then
|
||||
vim.api.nvim_out_write("[chatgpt_nvim:handler] File not found for deletion: " .. filepath .. "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.finish()
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
-- lua/chatgpt_nvim/init.lua
|
||||
local M = {}
|
||||
|
||||
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 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
|
||||
@@ -19,61 +26,444 @@ local function parse_response(raw)
|
||||
local ok, data = pcall(lyaml.load, raw)
|
||||
if not ok or not data then
|
||||
vim.api.nvim_err_writeln("Failed to parse YAML response.")
|
||||
ui.debug_log("RAW response that failed parsing:\n" .. raw)
|
||||
return nil
|
||||
end
|
||||
ui.debug_log("Successfully parsed YAML response.")
|
||||
return data
|
||||
end
|
||||
|
||||
function M.run_chatgpt_command()
|
||||
local function is_subpath(root, path)
|
||||
local root_abs = vim.fn.fnamemodify(root, ":p")
|
||||
local target_abs = vim.fn.fnamemodify(path, ":p")
|
||||
return target_abs:sub(1, #root_abs) == root_abs
|
||||
end
|
||||
|
||||
local function read_file(path)
|
||||
local fd = vim.loop.fs_open(path, "r", 438)
|
||||
if not fd then
|
||||
return nil
|
||||
end
|
||||
local stat = vim.loop.fs_fstat(fd)
|
||||
if not stat then
|
||||
vim.loop.fs_close(fd)
|
||||
return nil
|
||||
end
|
||||
local data = vim.loop.fs_read(fd, stat.size, 0)
|
||||
vim.loop.fs_close(fd)
|
||||
return data
|
||||
end
|
||||
|
||||
-- Added function to close existing buffers matching a name pattern.
|
||||
local function close_existing_buffer_by_name(name_pattern)
|
||||
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
|
||||
|
||||
local function handle_step_by_step_if_needed(prompt, conf)
|
||||
local token_count = estimate_token_count(prompt)
|
||||
local limit = conf.max_token or 2048
|
||||
if (not conf.enable_step_by_step) or (token_count <= limit) then
|
||||
return { prompt }
|
||||
end
|
||||
return { prompts["step-prompt"] }
|
||||
end
|
||||
|
||||
local function run_chatgpt_command()
|
||||
package.loaded["chatgpt_nvim.config"] = nil
|
||||
local config = require("chatgpt_nvim.config")
|
||||
local conf = config.load()
|
||||
local user_input = vim.fn.input("Message for O1 Model: ")
|
||||
if user_input == "" then
|
||||
print("No input provided.")
|
||||
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, conf)
|
||||
if #dirs == 0 then
|
||||
dirs = conf.directories
|
||||
end
|
||||
end
|
||||
|
||||
close_existing_buffer_by_name("ChatGPT_Prompt.md$")
|
||||
local bufnr = vim.api.nvim_create_buf(false, false)
|
||||
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, "omnifunc", "v:lua.chatgpt_file_complete")
|
||||
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
|
||||
vim.api.nvim_buf_set_option(bufnr, "buftype", "")
|
||||
vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||
"# 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."
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = bufnr,
|
||||
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 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 dirs = conf.directories or {"."}
|
||||
local project_structure = context.get_project_structure(dirs)
|
||||
local all_files = context.get_project_files(dirs)
|
||||
local file_sections = context.get_file_contents(all_files)
|
||||
local current_file_content, current_file_path = context.get_current_file()
|
||||
local readme_content = context.get_readme_content()
|
||||
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||
|
||||
local sections = {
|
||||
conf.initial_prompt .. "\n" .. user_input,
|
||||
"\n\nProject Structure:\n",
|
||||
project_structure,
|
||||
"\n\nBelow are the files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n"
|
||||
}
|
||||
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")
|
||||
|
||||
table.insert(sections, file_sections)
|
||||
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)
|
||||
|
||||
local prompt = table.concat(sections, "\n")
|
||||
copy_to_clipboard(prompt)
|
||||
print("Prompt copied to clipboard! Paste it into the ChatGPT O1 model.")
|
||||
copy_to_clipboard(chunks[1])
|
||||
if #chunks == 1 then
|
||||
vim.api.nvim_out_write("Prompt copied to clipboard! Paste into ChatGPT.\n")
|
||||
else
|
||||
vim.api.nvim_out_write("Step-by-step prompt copied to clipboard!\n")
|
||||
end
|
||||
|
||||
function M.run_chatgpt_paste_command()
|
||||
vim.api.nvim_buf_set_option(bufnr, "modified", false)
|
||||
end
|
||||
})
|
||||
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
end
|
||||
|
||||
local function run_chatgpt_paste_command()
|
||||
package.loaded["chatgpt_nvim.config"] = nil
|
||||
local config = require("chatgpt_nvim.config")
|
||||
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)
|
||||
if not data or not data.files then
|
||||
vim.api.nvim_err_writeln("No 'files' field found in the YAML response.")
|
||||
local data = parse_response(raw, conf)
|
||||
if not data then
|
||||
return
|
||||
end
|
||||
|
||||
for _, fileinfo in ipairs(data.files) do
|
||||
if fileinfo.path and fileinfo.content then
|
||||
handler.write_file(fileinfo.path, fileinfo.content)
|
||||
print("Wrote file: " .. fileinfo.path)
|
||||
else
|
||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'path' and 'content'.")
|
||||
if data.tools then
|
||||
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_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 data.project_name and data.files then
|
||||
if data.project_name ~= conf.project_name then
|
||||
vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
local is_final = false
|
||||
for _, fileinfo in ipairs(data.files) do
|
||||
if fileinfo.content or fileinfo.delete == true then
|
||||
is_final = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if is_final then
|
||||
if conf.preview_changes then
|
||||
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()
|
||||
for _, b in ipairs(bufs) do
|
||||
local name = vim.api.nvim_buf_get_name(b)
|
||||
if name:match("ChatGPT_Changes_Preview$") then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end)
|
||||
if not closed then
|
||||
vim.api.nvim_err_writeln("Preview not closed in time. Aborting.")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local final_files = data.files
|
||||
if conf.partial_acceptance then
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
local root = vim.fn.getcwd()
|
||||
for _, fileinfo in ipairs(final_files) do
|
||||
if not fileinfo.path then
|
||||
vim.api.nvim_err_writeln("Invalid file entry. Must have 'path'.")
|
||||
else
|
||||
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
|
||||
else
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
local file_sections = {}
|
||||
for _, f in ipairs(requested_paths) do
|
||||
local path = root .. "/" .. f
|
||||
local content = read_file(path)
|
||||
if content then
|
||||
table.insert(file_sections, ("\nFile: `%s`\n```\n%s\n```\n"):format(f, content))
|
||||
else
|
||||
table.insert(file_sections, ("\nFile: `%s`\n```\n(Could not read file)\n```\n"):format(f))
|
||||
end
|
||||
end
|
||||
|
||||
local sections = {
|
||||
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, or use the 'tools:' approach. If you have all info, provide final changes or continue your instructions."
|
||||
}
|
||||
|
||||
local prompt = table.concat(sections, "\n")
|
||||
local length = #prompt
|
||||
ui.debug_log("Returning requested files. Character count: " .. length)
|
||||
|
||||
copy_to_clipboard(prompt)
|
||||
print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.")
|
||||
end
|
||||
else
|
||||
vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.")
|
||||
end
|
||||
end
|
||||
|
||||
local function run_chatgpt_current_buffer_command()
|
||||
package.loaded["chatgpt_nvim.config"] = nil
|
||||
local config = require("chatgpt_nvim.config")
|
||||
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, conf)
|
||||
if #dirs == 0 then
|
||||
dirs = conf.directories
|
||||
end
|
||||
end
|
||||
|
||||
local final_prompt = build_prompt(user_input, dirs, conf)
|
||||
local chunks = handle_step_by_step_if_needed(final_prompt, conf)
|
||||
|
||||
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 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
|
||||
vim.api.nvim_out_write("Prompt (from current buffer) copied to clipboard! Paste into ChatGPT.\n")
|
||||
else
|
||||
vim.api.nvim_out_write("Step-by-step prompt (from current buffer) copied to clipboard!\n")
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
604
lua/chatgpt_nvim/prompts.lua
Normal file
604
lua/chatgpt_nvim/prompts.lua
Normal file
@@ -0,0 +1,604 @@
|
||||
local M = {
|
||||
["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
|
||||
|
||||
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.
|
||||
]],
|
||||
["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
|
||||
|
||||
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 & Dependency 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.
|
||||
- If you refer to internal packages, use relative paths consistent with the module’s 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**
|
||||
- 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 `my_lib` to keep the business logic separate from the command-line interface.”).
|
||||
- If you modify existing files, specify precisely which changes or additions you are making.
|
||||
]],
|
||||
["typo3"] = [[
|
||||
### 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.
|
||||
]],
|
||||
["rust"] = [[
|
||||
### 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 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.
|
||||
- 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.
|
||||
]],
|
||||
["basic"] = [[
|
||||
### Basic Prompt
|
||||
|
||||
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.
|
||||
|
||||
#### 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
|
||||
project_name: "%PROJECT_NAME%"
|
||||
```
|
||||
This must be part of every YAML block you generate.
|
||||
|
||||
3. **Operations Must Appear in the Tools Array**
|
||||
- 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.
|
||||
|
||||
4. **Read Before Write Rule**
|
||||
- **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 file’s contents, always use the following YAML format:
|
||||
```yaml
|
||||
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.
|
||||
|
||||
6. **Modifying Files**
|
||||
- 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: |
|
||||
# Full updated file content here
|
||||
```
|
||||
- Alternatively, for incremental changes, use:
|
||||
```yaml
|
||||
project_name: "%PROJECT_NAME%"
|
||||
tools:
|
||||
- tool: "replace_in_file"
|
||||
path: "relative/path/to/file"
|
||||
replacements:
|
||||
- search: "old text"
|
||||
replace: "new text"
|
||||
```
|
||||
|
||||
7. **Executing Commands**
|
||||
- To run any shell command (e.g., testing, listing files), use:
|
||||
```yaml
|
||||
project_name: "%PROJECT_NAME%"
|
||||
tools:
|
||||
- tool: "execute_command"
|
||||
command: "shell command here"
|
||||
```
|
||||
|
||||
8. **General Process**
|
||||
- **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**
|
||||
|
||||
#### Example YAML Block
|
||||
|
||||
```yaml
|
||||
project_name: "%PROJECT_NAME%"
|
||||
tools:
|
||||
- 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 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
|
||||
36
lua/chatgpt_nvim/tools/execute_command.lua
Normal file
36
lua/chatgpt_nvim/tools/execute_command.lua
Normal 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
|
||||
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 = "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
|
||||
175
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal file
175
lua/chatgpt_nvim/tools/lsp_robust_diagnostics.lua
Normal 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
|
||||
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 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
|
||||
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
|
||||
84
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal file
84
lua/chatgpt_nvim/tools/replace_in_file.lua
Normal 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
|
||||
98
lua/chatgpt_nvim/ui.lua
Normal file
98
lua/chatgpt_nvim/ui.lua
Normal file
@@ -0,0 +1,98 @@
|
||||
local M = {}
|
||||
-- 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
|
||||
|
||||
-- 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 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 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, conf)
|
||||
local selected_dirs = {}
|
||||
local selection_instructions = prompts["file-selection-instructions"]
|
||||
local lines = { selection_instructions }
|
||||
|
||||
for _, d in ipairs(dirs) do
|
||||
table.insert(lines, d)
|
||||
end
|
||||
|
||||
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
|
||||
vim.api.nvim_buf_delete(buf, {force = true})
|
||||
end
|
||||
end
|
||||
|
||||
local bufnr = vim.api.nvim_create_buf(false, false)
|
||||
vim.api.nvim_buf_set_name(bufnr, "ChatGPT_File_Selection")
|
||||
vim.api.nvim_buf_set_option(bufnr, "filetype", "markdown")
|
||||
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
|
||||
local function on_write()
|
||||
local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local final_dirs = {}
|
||||
for _, l in ipairs(new_lines) do
|
||||
if l ~= "" and not l:match("^Delete lines") and not l:match("^#") then
|
||||
table.insert(final_dirs, l)
|
||||
end
|
||||
end
|
||||
selected_dirs = final_dirs
|
||||
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(30000, 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 selected_dirs
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,2 +1,3 @@
|
||||
command! ChatGPT lua require('chatgpt_nvim').run_chatgpt_command()
|
||||
command! ChatGPTPaste lua require('chatgpt_nvim').run_chatgpt_paste_command()
|
||||
command! ChatGPTCurrentBuffer lua require('chatgpt_nvim').run_chatgpt_current_buffer_command()
|
||||
|
||||
Reference in New Issue
Block a user