Revert "feat: change to diff workflow"

This reverts commit df206dce88.
This commit is contained in:
2025-01-04 19:13:26 +01:00
parent a77dbb683d
commit 452253cdd0
4 changed files with 87 additions and 174 deletions

View File

@@ -1,5 +1,5 @@
<!-- README.md --> <!-- README.md -->
# ChatGPT NeoVim Plugin (Extensively Updated with Step-by-Step Prompting and Diff-Based Changes) # ChatGPT NeoVim Plugin (Extensively Updated with Step-by-Step Prompting)
This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to: This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to:
@@ -10,26 +10,21 @@ This plugin integrates a ChatGPT O1 model workflow into Neovim. It allows you to
- Any **initial files** you define (e.g., `README.md`, etc.) - Any **initial files** you define (e.g., `README.md`, etc.)
2. Copy these prompts to your clipboard to paste into ChatGPT O1. 2. Copy these prompts to your clipboard to paste into ChatGPT O1.
3. Receive **YAML changes from ChatGPT that include diffs**, then run `:ChatGPTPaste` to apply them or supply additional files. 3. Receive YAML changes from ChatGPT, then run `:ChatGPTPaste` to apply them or supply additional files.
- If youre updating an existing file, provide a `diff` field in the YAML.
- If youre creating a new file, use the `content` field.
- If youre deleting a file, use `delete: true`.
## New Key Features ## New Key Features
- **Step-by-Step Prompting** (`enable_step_by_step: true`): - **Step-by-Step Prompting** (`enable_step_by_step: true`):
If the request grows too large (exceeds `token_limit`), the plugin automatically generates a special prompt asking the model to split the task into smaller steps, working through them one by one. If the request grows too large (exceeds `token_limit`), the plugin automatically generates a special prompt asking the model to split the task into smaller steps, working through them one by one. This approach helps you stay within the models maximum token limit without having to manually break things apart.
- **Partial Acceptance**: If `partial_acceptance: true`, you can open a buffer that lists final diffs or file creations. You can remove or comment out lines you dont want, then only those changes are applied. - **Partial Acceptance**: If `partial_acceptance: true`, you can open a buffer that lists the final changes. Remove or comment out lines you dont want, then only those changes are applied.
- **Preview Changes**: If `preview_changes: true`, you get a buffer showing proposed diffs or new file content before you apply them. - **Preview Changes**: If `preview_changes: true`, you get a buffer showing proposed changes before you apply them.
- **Interactive File Selection**: If `interactive_file_selection: true`, you choose which directories from `.chatgpt_config.yaml` get included in the prompt, reducing token usage. - **Interactive File Selection**: If `interactive_file_selection: true`, you choose which directories from `.chatgpt_config.yaml` get included in the prompt, reducing token usage.
- **Improved Debug**: If `improved_debug: true`, debug logs go into a dedicated `ChatGPT_Debug_Log` buffer for easier reading. - **Improved Debug**: If `improved_debug: true`, debug logs go into a dedicated `ChatGPT_Debug_Log` buffer for easier reading.
- **Diff-Based Changes**: Rather than supplying entire file content for edits, you can include a `diff` in the YAML response. This allows you to see exactly what changed line by line and accept or reject it.
## Example `.chatgpt_config.yaml` ## Example `.chatgpt_config.yaml`
```yaml ```yaml
@@ -54,7 +49,7 @@ token_limit: 3000
1. **`:ChatGPT`** 1. **`:ChatGPT`**
- If `interactive_file_selection` is on, youll pick directories to include in a buffer named `ChatGPT_File_Selection`. - If `interactive_file_selection` is on, youll pick directories to include in a buffer named `ChatGPT_File_Selection`.
- Save & close with `:wq`, `:x`, or `:bd`. - Save & close with `:wq`, `:x`, or `:bd` (you dont have to use `:q`).
- If `enable_step_by_step` is on and the prompt might exceed `token_limit`, the plugin will generate instructions prompting the model to address each step separately. - If `enable_step_by_step` is on and the prompt might exceed `token_limit`, the plugin will generate instructions prompting the model to address each step separately.
2. **Paste Prompt to ChatGPT** 2. **Paste Prompt to ChatGPT**
@@ -65,7 +60,7 @@ token_limit: 3000
- If final changes are provided: - If final changes are provided:
- Optionally preview them (`preview_changes`). - Optionally preview them (`preview_changes`).
- Optionally partially accept them (`partial_acceptance`). - Optionally partially accept them (`partial_acceptance`).
- The plugin then applies file creation or deletion, or applies diffs to existing files. - Then the plugin writes/deletes files as specified.
## Troubleshooting & Tips ## Troubleshooting & Tips
- Adjust `token_limit` in `.chatgpt_config.yaml` as needed. - Adjust `token_limit` in `.chatgpt_config.yaml` as needed.
@@ -85,6 +80,6 @@ commands:
pattern: "searchString" pattern: "searchString"
target: "path/to/file/or/directory" target: "path/to/file/or/directory"
``` ```
The **list** command uses the system's `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory. The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
Enjoy the improved, more flexible ChatGPT Neovim plugin with step-by-step and diff-based support! Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!

View File

@@ -6,7 +6,9 @@ local ok_yaml, lyaml = pcall(require, "lyaml")
local prompt_blocks = { local prompt_blocks = {
["go-development"] = [[ ["go-development"] = [[
You are a coding assistant specialized in Go development. You are a coding assistant specialized in Go development.
You will receive a projects context and user instructions related to Go code. You will receive a projects context and user instructions related to Go code,
and you must return the requested modifications or guidance.
When returning modifications, follow the specified YAML structure.
Keep your suggestions aligned with Go best practices and idiomatic Go. Keep your suggestions aligned with Go best practices and idiomatic Go.
]], ]],
["typo3-development"] = [[ ["typo3-development"] = [[
@@ -17,36 +19,51 @@ local prompt_blocks = {
]], ]],
["rust-development"] = [[ ["rust-development"] = [[
You are a coding assistant specialized in Rust development. You are a coding assistant specialized in Rust development.
You will receive a projects context and user instructions related to Rust code. You will receive a projects context and user instructions related to Rust code,
Keep your suggestions aligned with Rust best practices and idiomatic Rust. and you must return the requested modifications or guidance.
When returning modifications, follow the specified YAML structure.
Keep your suggestions aligned with Rust best practices and idiomatic Rust.
]], ]],
["basic-prompt"] = [[ ["basic-prompt"] = [[
You are a coding assistant who receives a project's context and user instructions. You are a coding assistant who receives a project's context and user instructions.
The user will provide a prompt, and you will guide them through a workflow: The user will provide a prompt, and you will guide them through a workflow:
1. Analyse which files you need. Ask for file contents in YAML if needed. 1. First, you should analyse which files you need to solve the request.
2. Request additional context outside YAML if necessary. You can see which files are present in the provided project structure.
3. When ready, provide final changes in YAML with: Additionally if presented you could also ask for files of a library which is provided in for example composer.json.
- 'project_name' 2. If file contents is needed provide a yaml which asks for the file contents.
- 'files', each having: For example:
* 'path' project_name: example_project
* 'diff' (for patching an existing file) OR 'content' (for a new file) OR 'delete' files:
Important: do not provide entire file content for updates; instead provide a unified diff in 'diff'. - path: "relative/path/to/file"
Only modify or delete files whose contents you have explicitly requested and seen beforehand. 3. If more information or context is needed, ask the user (outside of the YAML) to provide that.
4. When all necessary information is gathered, provide the final YAML with the
project's name and a list of files to be created or modified.
Also explain the changes you made below the yaml.
The final YAML must have a top-level key named 'project_name' that matches the project's configured name,
and a top-level key named 'files', which is a list of file changes. Each element in 'files' must be a mapping with:
- 'path' for the file path relative to the projects root directory.
- either 'content' with a multiline string for new content, or 'delete: true' if the file should be deleted.
Important: dont use comments in the code to explain which steps you have taken.
Comments should just explain the code and not your thought process.
You can explain your thought process outside of the YAML.
If more context is needed at any point before providing the final YAML, request it outside of the YAML.
Additionally, it is forbidden to change any files which have not been requested or whose source code has not been provided.
]], ]],
["secure-coding"] = [[ ["secure-coding"] = [[
You are a coding assistant specialized in secure software development. You are a coding assistant specialized in secure software development.
Always consider security impacts. Use diffs for updates, new content for new files, As you generate code or provide guidance, you must consider the security impact of every decision.
and 'delete: true' for removals. You will write and review code with a focus on minimizing vulnerabilities and following best security practices,
such as validating all user inputs, avoiding unsafe libraries or functions, and following secure coding standards.
]], ]],
["workflow-prompt"] = [[ ["workflow-prompt"] = [[
You are a coding assistant focusing on making the Neovim ChatGPT workflow straightforward and user-friendly. You are a coding assistant focusing on making the Neovim ChatGPT workflow straightforward and user-friendly.
Remind the user to: Provide a concise set of steps or guidance, reminding the user:
- List needed files for further context - How to list needed files for further context
- Request additional information outside YAML if needed - How to request additional information outside of the YAML
- Provide final changes in YAML with 'project_name' and 'files', using: - How to finalize changes with a YAML response containing project_name and files
* 'diff' for existing file modifications Always ensure that prompts and explanations remain clear and minimal, reducing user errors.
* 'content' for new files
* 'delete: true' for file deletions
]] ]]
} }
@@ -86,6 +103,7 @@ function M.load()
initial_prompt = "", initial_prompt = "",
directories = { "." }, directories = { "." },
default_prompt_blocks = {}, default_prompt_blocks = {},
-- Changed default from 128000 to 16384 as requested
token_limit = 16384, token_limit = 16384,
project_name = "", project_name = "",
debug = false, debug = false,
@@ -95,6 +113,7 @@ function M.load()
partial_acceptance = false, partial_acceptance = false,
improved_debug = false, improved_debug = false,
enable_chunking = false, enable_chunking = false,
-- New default for step-by-step
enable_step_by_step = true, enable_step_by_step = true,
enable_debug_commands = false enable_debug_commands = false
} }
@@ -142,6 +161,7 @@ function M.load()
if type(result.enable_chunking) == "boolean" then if type(result.enable_chunking) == "boolean" then
config.enable_chunking = result.enable_chunking config.enable_chunking = result.enable_chunking
end end
-- Added logic to load enable_step_by_step from user config
if type(result.enable_step_by_step) == "boolean" then if type(result.enable_step_by_step) == "boolean" then
config.enable_step_by_step = result.enable_step_by_step config.enable_step_by_step = result.enable_step_by_step
end end
@@ -154,6 +174,7 @@ function M.load()
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..." config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
end end
-- Merge the default prompt blocks with the config's initial prompt
if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
local merged_prompt = {} local merged_prompt = {}
for _, block_name in ipairs(config.default_prompt_blocks) do for _, block_name in ipairs(config.default_prompt_blocks) do

View File

@@ -69,76 +69,6 @@ function M.delete_file(filepath)
end end
end end
-- Applies a unified diff to the specified file.
-- This spawns an external 'patch' command if available.
function M.apply_diff(filepath, diff_content)
local conf = config.load()
local tmp_original = vim.fn.tempname()
local tmp_patch = vim.fn.tempname()
-- Read original file (or empty if it doesn't exist)
local fd_in = uv.fs_open(filepath, "r", 438)
local original_data = ""
if fd_in then
local stat = uv.fs_fstat(fd_in)
original_data = uv.fs_read(fd_in, stat.size, 0)
uv.fs_close(fd_in)
end
-- Write original content to temp file
local fd_orig = uv.fs_open(tmp_original, "w", 438)
if fd_orig then
uv.fs_write(fd_orig, original_data, -1)
uv.fs_close(fd_orig)
end
-- Write diff to temp file
local fd_patch = uv.fs_open(tmp_patch, "w", 438)
if fd_patch then
uv.fs_write(fd_patch, diff_content, -1)
uv.fs_close(fd_patch)
else
return false, "Could not open temporary file to write patch."
end
-- Attempt to run 'patch'
local patch_cmd = "patch -u " .. vim.fn.shellescape(tmp_original) .. " < " .. vim.fn.shellescape(tmp_patch)
local handle = io.popen(patch_cmd)
if not handle then
return false, "Failed to run patch command."
end
local result = handle:read("*a")
local success_close, errmsg = handle:close()
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command output:\n" .. (result or "") .. "\n")
end
if not success_close then
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:handler] Patch command failed: " .. (errmsg or "unknown") .. "\n")
end
return false, errmsg
end
-- If successful, read the patched file and write it back
local fd_out = uv.fs_open(tmp_original, "r", 438)
if fd_out then
local stat_out = uv.fs_fstat(fd_out)
local new_data = uv.fs_read(fd_out, stat_out.size, 0)
uv.fs_close(fd_out)
M.write_file(filepath, new_data)
if conf.debug then
vim.api.nvim_out_write("[chatgpt_nvim:handler] Successfully applied patch to: " .. filepath .. "\n")
end
else
return false, "Could not read patched file."
end
return true
end
function M.finish() function M.finish()
print("Finished processing files.") print("Finished processing files.")
end end

View File

@@ -111,30 +111,17 @@ local function preview_changes(changes)
"" ""
}) })
for _, fileinfo in ipairs(changes) do for _, fileinfo in ipairs(changes) do
if fileinfo.delete then local indicator = (fileinfo.delete == true) and "Delete file" or "Write file"
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
string.format("=== Delete file: %s ===", fileinfo.path or "<no path>"), string.format("=== %s: %s ===", indicator, fileinfo.path or "<no path>")
"" })
}) if fileinfo.content then
elseif fileinfo.diff then
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
string.format("=== Diff for file: %s ===", fileinfo.path or "<no path>")
})
local lines = vim.split(fileinfo.diff, "\n")
for _, line in ipairs(lines) do
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line })
end
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
elseif fileinfo.content then
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
string.format("=== New file: %s ===", fileinfo.path or "<no path>")
})
local lines = vim.split(fileinfo.content, "\n") local lines = vim.split(fileinfo.content, "\n")
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line }) vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line })
end end
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
end end
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { "" })
end end
vim.cmd("vsplit") vim.cmd("vsplit")
@@ -152,16 +139,9 @@ local function partial_accept(changes)
"" ""
} }
for _, fileinfo in ipairs(changes) do for _, fileinfo in ipairs(changes) do
if fileinfo.delete then local action = (fileinfo.delete == true) and "[DELETE]" or "[WRITE]"
table.insert(lines, string.format("[DELETE] %s", fileinfo.path or "<no path>")) table.insert(lines, string.format("%s %s", action, fileinfo.path or "<no path>"))
elseif fileinfo.diff then if fileinfo.content then
table.insert(lines, string.format("[DIFF] %s", fileinfo.path or "<no path>"))
local diff_lines = vim.split(fileinfo.diff, "\n")
for _, dl in ipairs(diff_lines) do
table.insert(lines, " " .. dl)
end
elseif fileinfo.content then
table.insert(lines, string.format("[WRITE] %s", fileinfo.path or "<no path>"))
local content_lines = vim.split(fileinfo.content, "\n") local content_lines = vim.split(fileinfo.content, "\n")
for _, cl in ipairs(content_lines) do for _, cl in ipairs(content_lines) do
table.insert(lines, " " .. cl) table.insert(lines, " " .. cl)
@@ -176,53 +156,46 @@ local function partial_accept(changes)
local function on_write() local function on_write()
local edited_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local edited_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local keep_current = false local keep_current = false
local current_fileinfo = { path = nil, delete = false, diff = nil, content = nil } local current_fileinfo = { path = nil, content = nil, delete = false }
local accum = {} local content_accum = {}
for _, line in ipairs(edited_lines) do for _, line in ipairs(edited_lines) do
if line:match("^#") or line == "" then if line:match("^#") or line == "" then
goto continue goto continue
end end
local del_match = line:match("^%[DELETE%] (.+)") local del_match = line:match("^%[DELETE%] (.+)")
local diff_match = line:match("^%[DIFF%] (.+)")
local write_match = line:match("^%[WRITE%] (.+)") local write_match = line:match("^%[WRITE%] (.+)")
if del_match then
if del_match or diff_match or write_match then
-- store previous if any
if keep_current and (current_fileinfo.path ~= nil) then if keep_current and (current_fileinfo.path ~= nil) then
if current_fileinfo.diff then if #content_accum > 0 then
current_fileinfo.diff = table.concat(accum, "\n") current_fileinfo.content = table.concat(content_accum, "\n")
elseif current_fileinfo.content then
current_fileinfo.content = table.concat(accum, "\n")
end end
table.insert(final_changes, current_fileinfo) table.insert(final_changes, current_fileinfo)
end end
accum = {}
keep_current = true keep_current = true
current_fileinfo = { path = del_match, delete = true, content = nil }
if del_match then content_accum = {}
current_fileinfo = { path = del_match, delete = true, diff = nil, content = nil } elseif write_match then
elseif diff_match then if keep_current and (current_fileinfo.path ~= nil) then
current_fileinfo = { path = diff_match, delete = false, diff = "", content = nil } if #content_accum > 0 then
elseif write_match then current_fileinfo.content = table.concat(content_accum, "\n")
current_fileinfo = { path = write_match, delete = false, diff = nil, content = "" } end
table.insert(final_changes, current_fileinfo)
end end
keep_current = true
current_fileinfo = { path = write_match, delete = false, content = nil }
content_accum = {}
else else
if keep_current then if keep_current then
table.insert(accum, line:gsub("^%s*", "")) table.insert(content_accum, line:gsub("^%s*", ""))
end end
end end
::continue:: ::continue::
end end
if keep_current and (current_fileinfo.path ~= nil) then if keep_current and (current_fileinfo.path ~= nil) then
if current_fileinfo.diff ~= nil then if #content_accum > 0 then
current_fileinfo.diff = table.concat(accum, "\n") current_fileinfo.content = table.concat(content_accum, "\n")
elseif current_fileinfo.content ~= nil then
current_fileinfo.content = table.concat(accum, "\n")
end end
table.insert(final_changes, current_fileinfo) table.insert(final_changes, current_fileinfo)
end end
@@ -426,7 +399,7 @@ function M.run_chatgpt_command()
```yaml ```yaml
commands: commands:
- command: "list" - command: "ls"
dir: "some/directory" dir: "some/directory"
- command: "grep" - command: "grep"
@@ -434,6 +407,7 @@ function M.run_chatgpt_command()
target: "path/to/file/or/directory" target: "path/to/file/or/directory"
``` ```
The "ls" command uses the system's 'ls' command to list directory contents.
When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard. When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard.
]]) ]])
end end
@@ -493,7 +467,7 @@ function M.run_chatgpt_paste_command()
local is_final = false local is_final = false
for _, fileinfo in ipairs(data.files) do for _, fileinfo in ipairs(data.files) do
if fileinfo.content or fileinfo.delete == true or fileinfo.diff then if fileinfo.content or fileinfo.delete == true then
is_final = true is_final = true
break break
end end
@@ -544,20 +518,12 @@ function M.run_chatgpt_paste_command()
ui.debug_log("Deleting file: " .. fileinfo.path) ui.debug_log("Deleting file: " .. fileinfo.path)
handler.delete_file(fileinfo.path) handler.delete_file(fileinfo.path)
print("Deleted: " .. fileinfo.path) print("Deleted: " .. fileinfo.path)
elseif fileinfo.diff then
ui.debug_log("Applying diff to file: " .. fileinfo.path)
local success, err = handler.apply_diff(fileinfo.path, fileinfo.diff)
if not success then
vim.api.nvim_err_writeln("Error applying diff: " .. (err or "unknown"))
else
print("Patched: " .. fileinfo.path)
end
elseif fileinfo.content then elseif fileinfo.content then
ui.debug_log("Writing new file: " .. fileinfo.path) ui.debug_log("Writing file: " .. fileinfo.path)
handler.write_file(fileinfo.path, fileinfo.content) handler.write_file(fileinfo.path, fileinfo.content)
print("Wrote: " .. fileinfo.path) print("Wrote: " .. fileinfo.path)
else else
vim.api.nvim_err_writeln("Invalid file entry. Must have 'diff', 'content', or 'delete'.") vim.api.nvim_err_writeln("Invalid file entry. Must have 'content' or 'delete'.")
end end
::continue:: ::continue::
end end
@@ -587,7 +553,7 @@ function M.run_chatgpt_paste_command()
"\n\nProject name: " .. (conf.project_name or ""), "\n\nProject name: " .. (conf.project_name or ""),
"\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n", "\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n",
table.concat(file_sections, "\n"), table.concat(file_sections, "\n"),
"\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (use `diff`, `content`, or `delete`) to apply changes.\n" "\n\nIf you need more files, please respond again in YAML listing additional files. If you have all information you need, provide the final YAML with `project_name` and `files` (with `content` or `delete`) to apply changes.\n"
} }
local prompt = table.concat(sections, "\n") local prompt = table.concat(sections, "\n")
@@ -681,6 +647,7 @@ function M.run_chatgpt_current_buffer_command()
target: "path/to/file/or/directory" target: "path/to/file/or/directory"
``` ```
The "list" command uses the system's 'ls' command to list directory contents.
When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard. When these commands are present and enable_debug_commands is true, I'll execute them and return the results in the clipboard.
]]) ]])
end end