Skip to content

Add: apply_patch tool#3045

Draft
ElliottLester wants to merge 6 commits intoolimorris:mainfrom
ElliottLester:apply_patch_tool
Draft

Add: apply_patch tool#3045
ElliottLester wants to merge 6 commits intoolimorris:mainfrom
ElliottLester:apply_patch_tool

Conversation

@ElliottLester
Copy link
Copy Markdown

Description

  • New Tool: Added lua/codecompanion/interactions/chat/tools/builtin/apply_patch.lua implementing a custom patch format with fuzzy matching (exact, rstrip, trim, and unicode normalization) to increase application reliability.
  • Configuration: Registered apply_patch in lua/codecompanion/config.lua and added it to the default tool groups.
  • Documentation: Created lua/codecompanion/interactions/chat/tools/builtin/apply_patch.md to provide the LLM with the patch language specification.
  • Testing: Added a comprehensive test suite in tests/interactions/chat/tools/test_apply_patch.lua covering parsing, file creation, deletion, updates, and renaming.
  • Planning: Added apply_patch_plan.md documenting the technical specification and implementation strategy.

AI Usage

opencode: gemma-4-31B-it

Related Issue(s)

N/A

Screenshots

N/A

Checklist

  • I've read the contributing guidelines and have adhered to them in this PR
  • I confirm that this PR has been majority created by me, and not AI (unless stated in the "AI Usage" section above)
  • I've run make all to ensure docs are generated, tests pass and StyLua has formatted the code
  • (optional) I've added test coverage for this fix/feature
  • (optional) I've updated the README and/or relevant docs pages

Implement a structured patch application tool that allows the LLM to
add, delete, and update files in the codebase.

- Implement `apply_patch` tool logic with support for `*** Add File`,
`*** Delete File`, and `*** Update File` (including `*** Move to`)
operations.
- Add a robust seeking mechanism for updates using a fallback hierarchy:
Exact Match $\rightarrow$ RStrip $\rightarrow$ Trim $\rightarrow$
Normalized Unicode.
- Register the tool in `config.lua` and provide a comprehensive system
prompt.
- Add unit tests covering parsing, file creation, deletion, updates, and
renames.
- Include `apply_patch_plan.md` detailing the tool specification.
- Add type annotations for the `ApplyPatch` tool.
- Validate normalized paths and move paths to prevent invalid file
operations.
- Reorganize imports to the top of the file for better consistency.
- Enhance error messages when patch context is not found.
Extract hunk handling logic into dedicated helper functions
(`handle_add`, `handle_delete`, and `handle_update`) to improve
readability and maintainability. Update associated tests to align with
the refactored structure and use more representative test cases.
Implement a check to ensure that patch hunks match exactly one location
in the target file. If multiple matches are found for the old lines, an
error is returned requesting more context to resolve the ambiguity.

- Add `count_sequences` helper to detect multiple pattern occurrences.
- Update `handle_update` to validate match uniqueness.
- Add test case to verify failure on ambiguous matches.
child.lua([=[
local apply_patch = require('codecompanion.interactions.chat.tools.builtin.apply_patch')
-- Setup: create file with duplicate lines
vim.fn.mkdir('test_patch_dir', 'p')
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use vim.fn.tempname() to prevent the workspace getting polluted with test artifacts:

I typically do:

  local tmpdir = child.lua("return vim.fn.tempname()")
  child.fn.mkdir(tmpdir)
  local f1 = vim.fs.joinpath(tmpdir, "one.txt")
  local f2 = vim.fs.joinpath(tmpdir, "two.md")
  child.fn.writefile({ "first file" }, f1)
  child.fn.writefile({ "second file" }, f2)

local file_utils = require("codecompanion.utils.files")
local fmt = string.format
local tool_helpers = require("codecompanion.interactions.chat.tools.builtin.helpers")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend including a large comment header in this file to explain how the patching process works. Anyone coming to this cold will have to work hard to understand how it functions. Method names like seek_sequence aren't immediately clear as to why and what they're doing

return s
end

local function try_match(lines, pattern, start_index, compare_fn, eof)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a key function, I generally prefer parameters to be in an opts table. This makes it pretty trivial to update in the future. Something like:

try_match(opts)
-- Or
try_match(lines, opts)

gives you a bit of versatility and matches what we try and do throughout CodeCompanion. Surprised Opencode didn't pick this up, as it is in AGENTS.md.

if vim.fn.getfsize(path) == -1 then
return { status = "error", data = "File to delete does not exist: " .. path }
end
vim.fn.delete(path, "rf")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use file_utils.delete(path) in place of vim functions. They should be faster and utilize existing utilities.

Copy link
Copy Markdown
Owner

@olimorris olimorris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really awesome. I'm going to test it out tonightand see how it performs with Anthropic, OpenAI, Copilot etc.

I've put some inline comments just afte a quick pass. My overarching feedback would be to ensure that we have LuaLS annotations and descriptions for each function/method. This is noted in AGENTS.md so looks like OpenCode missed it. Could probably do with more comments in apply_patch.lua as well, explaining why we're doing things.

You could also consider separating the file into tools/builtin/apply_patch/*.lua files if that would make it more managable.

@bassamsdata
Copy link
Copy Markdown
Contributor

Hello! Given that I had worked on the previous version of the edit tool, I thought I'd share a couple of observations that could prove useful during the validation process of the proposed algorithm.

Some edge cases that need to be considered (off the top of my head and I haven't read the code):

  1. Properly dealing with whitespace issues, tabs vs. spaces. This is extremely important, especially considering certain programming languages such as Python and Go. So, they can be considered a great choice for testing purposes.

  2. Dealing with comments of various types. Some examples could include Lua-style comments (--, ---, ---@), comments in Python code, etc.

  3. Testing on a variety of formatting styles that can include code of any type, from regular programming languages to Markdown, diff, Lisp, etc.

  4. In case the tool encounters multiple matches for the string being edited, how exactly should the tool behave? Is it supposed to replace all matches at once or just one? If only one, then which one – first occurrence from top to bottom or something else entirely?

  5. In case the tool fails to do it for whatever reason, does it provide any feedback or tips to an agent on how to handle the situation?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants