Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,87 @@ When `opencode` requests a permission, `opencode.nvim` waits for idle to ask you

#### Edits

For edit requests, `opencode.nvim` opens the target file in a new tab and uses Neovim's `:diffpatch` to display the proposed changes side-by-side. See `:h 'diffopt'` for customization.
For edit requests, `opencode.nvim` opens the target file in a new tab and displays the proposed changes with its built-in `:diffpatch` renderer by default. See `:h 'diffopt'` for customization.

You can replace the renderer with a custom function. The renderer receives a context and can return a session with optional hunk navigation, hunk actions, and cleanup hooks.

The context provides:

- `ctx.request_id`: the edit request id.
- `ctx.filepath`: normalized target filepath.
- `ctx.diff`: the unified diff from `opencode`.
- `ctx.proposed_text()`: lazily computes the patched file contents.
- `ctx.permit(reply)`: sends `"once"` or `"reject"`.
- `ctx.close()`: closes the active diff view.
- `ctx.open_default()`: opens the built-in `:diffpatch` renderer and returns its session.

The returned session can provide:

- `bufnr`
- `close()`
- `next_hunk()` / `prev_hunk()`
- `accept_hunk()` / `reject_hunk()`

Edit keymaps are global and operate on the currently active `opencode` edit session instead of being buffer-local. If there is no active edit diff, they show a notification instead of failing silently.

Default keymaps:

- `da`: accept the edit request
- `dr`: reject the edit request
- `q`: close the edit diff
- `dp`: accept the current hunk and reject the request
- `do`: reject the current hunk and reject the request
- `]c` / `[c`: next / previous hunk

You can override or disable them:

```lua
vim.g.opencode_opts = {
events = {
permissions = {
edits = {
keymaps = {
accept = "<leader>oa",
reject = "<leader>or",
close = "<leader>oq",
accept_hunk = false,
},
},
},
},
}
```

```lua
vim.g.opencode_opts = {
events = {
permissions = {
edits = {
renderer = require("opencode").diff_renderers.mini_diff(),
},
},
},
}
```

`mini.diff` users can optionally tweak the helper:

```lua
vim.g.opencode_opts = {
events = {
permissions = {
edits = {
renderer = require("opencode").diff_renderers.mini_diff({
open_cmd = "tabnew",
ensure_overlay = true,
}),
},
},
},
}
```

If you need full control, `renderer` still accepts a custom function using the `ctx` helpers above.

| Keymap | Function |
| ------- | ----------------------------------------------------------------------------- |
Expand Down
1 change: 1 addition & 0 deletions lua/opencode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,6 @@ end
--------------------

M.snacks_picker_send = require("opencode.integrations.pickers.snacks").send
M.diff_renderers = require("opencode.integrations.diff")

return M
10 changes: 10 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ local defaults = {
idle_delay_ms = 1000,
edits = {
enabled = true,
renderer = nil,
keymaps = {
accept = "da",
reject = "dr",
close = "q",
accept_hunk = "dp",
reject_hunk = "do",
next_hunk = "]c",
prev_hunk = "[c",
},
},
},
},
Expand Down
105 changes: 105 additions & 0 deletions lua/opencode/integrations/diff.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
local M = {}

---@class opencode.integrations.diff.MiniDiffOpts
---@field open_cmd? string Ex command used to open the target file. Defaults to `tabnew`.
---@field ensure_overlay? boolean Whether to enable `mini.diff` overlay while the edit session is active. Defaults to `true`.

---@param mini_diff table
---@param buf integer
---@param line integer
local function accept_hunk(mini_diff, buf, line)
mini_diff.do_hunks(buf, "reset", { line_start = line, line_end = line })
end

---@return fun(ctx: opencode.events.permissions.edits.Context): opencode.events.permissions.edits.Session?
function M.default()
return function(ctx)
return ctx.open_default()
end
end

---Create an edit renderer backed by `mini.diff`.
---
---Falls back to the built-in `:diffpatch` renderer when `mini.diff` is not available
---or the proposed text cannot be computed.
---
---@param opts? opencode.integrations.diff.MiniDiffOpts
---@return fun(ctx: opencode.events.permissions.edits.Context): opencode.events.permissions.edits.Session?
function M.mini_diff(opts)
opts = vim.tbl_extend("keep", opts or {}, {
open_cmd = "tabnew",
ensure_overlay = true,
})

local fallback = M.default()

return function(ctx)
local ok, mini_diff = pcall(require, "mini.diff")
if not ok or type(rawget(_G, "MiniDiff")) ~= "table" then
return fallback(ctx)
end

local proposed = ctx.proposed_text()
if not proposed then
return fallback(ctx)
end

vim.cmd(("%s %s"):format(opts.open_cmd, vim.fn.fnameescape(ctx.filepath)))
local bufnr = vim.api.nvim_get_current_buf()
local previous_state = mini_diff.get_buf_data(bufnr)
local previous_config = vim.deepcopy(vim.b[bufnr].minidiff_config)

pcall(mini_diff.disable, bufnr)
vim.b[bufnr].minidiff_config = { source = mini_diff.gen_source.none() }

local enabled = pcall(mini_diff.enable, bufnr)
local ref_ok = enabled and pcall(mini_diff.set_ref_text, bufnr, proposed)
if not ref_ok then
pcall(vim.cmd, "tabclose")
return fallback(ctx)
end

if opts.ensure_overlay then
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end

local buf_data = mini_diff.get_buf_data(bufnr) or {}
if not buf_data.overlay then
pcall(mini_diff.toggle_overlay, bufnr)
end
end)
end

return {
bufnr = bufnr,
close = function()
pcall(mini_diff.disable, bufnr)
vim.b[bufnr].minidiff_config = previous_config

if previous_state then
pcall(mini_diff.enable, bufnr)
if previous_state.ref_text ~= nil then
pcall(mini_diff.set_ref_text, bufnr, previous_state.ref_text)
end
if previous_state.overlay then
pcall(mini_diff.toggle_overlay, bufnr)
end
end
end,
next_hunk = function()
mini_diff.goto_hunk("next")
end,
prev_hunk = function()
mini_diff.goto_hunk("prev")
end,
accept_hunk = function()
local line = vim.api.nvim_win_get_cursor(0)[1]
accept_hunk(mini_diff, bufnr, line)
end,
}
end
end

return M
Loading
Loading