Skip to content
Merged
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,29 @@ Press `r` to select a range of turns/messages: move through user prompts, mark t

Press `q` to close the review tab. `:Opencode diff next` and `:Opencode diff prev` also select files in the open review tab.

Press `gf` in the file list or diff preview to open the selected working-tree file in a new tab, keeping the snapshot review intact. From a preview, it targets the corresponding post-change line (deleted lines use the next surviving line, or the last one), clamped to the current file's length. This is best-effort if the working tree has changed since the snapshot. Diff panes show absolute line numbers. If the file no longer exists on disk, a warning is shown instead. Remap this using the `open_file` action.

### Customize session-diff keymaps

Like panel mappings, `keymap.session_diff` binds keys to `{ action, { arguments }, mode = ..., desc = ..., nowait = ... }` entries, using the same keymap utility. Actions can be local diff action names, panel command names, or Lua callbacks; local diff actions take precedence. Set a default key to `false` to disable it. `<Tab>` / `<S-Tab>` select the next / previous file in the list and diff panes.

```lua
keymap = {
session_diff = {
preview = {
[']f'] = { 'next_file', desc = 'Next file' },
['[f'] = { 'prev_file', desc = 'Previous file' },
['<M-e>'] = { function() vim.cmd('wincmd h') end, desc = 'Focus file list' },
},
list = {
['<M-e>'] = { function() vim.cmd('wincmd l') end, desc = 'Focus diff' },
},
},
}
```

Dedicated filetypes are `opencode_diff_list`, `opencode_diff_messages`, `opencode_diff_help`, and `opencode_diff_comment`. Preview buffers use composite filetypes such as `lua.opencode_diff_preview`, `diff.opencode_diff_preview`, and `markdown.opencode_diff_message_preview`, keeping source syntax highlighting. Use `FileType` patterns `*.opencode_diff_preview` or `*.opencode_diff_message_preview` to attach your own buffer-local mappings; configured mappings are installed before these hooks run.

### Comment on a session diff

In a diff preview, press `c` on a line or visual selection to add or edit a review comment. Use `dc` to remove one; `]r` and `[r` move between comments. In the comment editor, `<C-s>` or `:w` saves, while `q` or `<Esc>` cancels. Comments appear as signs and in the file tree; closing the review focuses the input when comments are pending. Comments are sent with the next prompt and include the reviewed snapshot and a best-effort current-file status.
Expand Down
6 changes: 6 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ M.defaults = {
session_diff = {
list = {
['<CR>'] = { 'activate', desc = 'Open file or toggle folder' },
['gf'] = { 'open_file', desc = 'Open working-tree file in a new tab' },
['<Tab>'] = { 'next_file', desc = 'Next file' },
['<S-Tab>'] = { 'prev_file', desc = 'Previous file' },
['r'] = { 'toggle_range', desc = 'Choose message range', nowait = true },
['p'] = { 'toggle_view', desc = 'Toggle diff layout' },
['q'] = { 'close', desc = 'Close diff', nowait = true },
Expand All @@ -49,6 +52,9 @@ M.defaults = {
['g?'] = { 'toggle_help', desc = 'Toggle keymap help' },
},
preview = {
['gf'] = { 'open_file', desc = 'Open working-tree file in a new tab' },
['<Tab>'] = { 'next_file', desc = 'Next file' },
['<S-Tab>'] = { 'prev_file', desc = 'Previous file' },
['q'] = { 'close', desc = 'Close diff', nowait = true },
['p'] = { 'toggle_view', desc = 'Toggle diff layout' },
['g?'] = { 'toggle_help', desc = 'Toggle keymap help' },
Expand Down
30 changes: 22 additions & 8 deletions lua/opencode/keymap.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
local M = {}
local commands = require('opencode.commands')
local store = require('opencode.state.store')
local window_keymaps = {}

Expand All @@ -24,13 +23,21 @@ end

---@param func_name string|function
---@param func_args any
---@param actions? table<string, function?> Buffer-local overrides take precedence over panel commands
---@return function|nil
local function resolve_callback(func_name, func_args)
local function resolve_callback(func_name, func_args, actions)
if type(func_name) == 'string' and actions and actions[func_name] then
func_name = actions[func_name]
end
if type(func_name) == 'function' then
return func_name
return function()
local args = func_args and (type(func_args) == 'table' and vim.deepcopy(func_args) or { func_args }) or {}
return func_name(unpack(args))
end
end

if type(func_name) == 'string' then
local commands = require('opencode.commands')
local command_defs = commands.get_commands()
if command_defs[func_name] then
return function()
Expand All @@ -51,7 +58,10 @@ end
---@param default_modes table Default modes for these keymaps
---@param base_opts table Base options to use for all keymaps
---@param preserve_existing? boolean
local function process_keymap_entry(keymap_config, default_modes, base_opts, preserve_existing)
---@param actions? table<string, function?>
local function process_keymap_entry(keymap_config, default_modes, base_opts, preserve_existing, actions)
-- Commands load diff handlers, which also use this mapper for their buffers.
local commands = require('opencode.commands')
local command_defs = commands.get_commands()

for key_binding, config_entry in pairs(keymap_config) do
Expand All @@ -60,7 +70,7 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, pre
elseif config_entry then
local func_name = config_entry[1]
local func_args = config_entry[2]
local callback = resolve_callback(func_name, func_args)
local callback = resolve_callback(func_name, func_args, actions)

local modes = config_entry.mode or default_modes
if preserve_existing and base_opts.buffer then
Expand All @@ -81,7 +91,10 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, pre
end
local opts = vim.tbl_deep_extend('force', {}, base_opts)
opts.nowait = config_entry.nowait
opts.desc = config_entry.desc or vim.tbl_get(command_defs, func_name, 'desc') or ''
local command_desc = type(func_name) == 'string'
and not (actions and actions[func_name])
and vim.tbl_get(command_defs, func_name, 'desc')
opts.desc = config_entry.desc or command_desc or ''

if not callback then
if type(func_name) ~= 'string' then
Expand Down Expand Up @@ -131,12 +144,13 @@ end
---@param keymap_config table Window keymap configuration
---@param buf_id integer Buffer ID to set keymaps for
---@param preserve_existing? boolean
function M.setup_window_keymaps(keymap_config, buf_id, preserve_existing)
---@param actions? table<string, function?> Local action overrides; unknown names use command resolution and its error notification
function M.setup_window_keymaps(keymap_config, buf_id, preserve_existing, actions)
if not vim.api.nvim_buf_is_valid(buf_id) then
return
end

process_keymap_entry(keymap_config or {}, { 'n' }, { silent = true, buffer = buf_id }, preserve_existing)
process_keymap_entry(keymap_config or {}, { 'n' }, { silent = true, buffer = buf_id }, preserve_existing, actions)
end

return M
15 changes: 13 additions & 2 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@
---@field project SessionProjectInfo|nil

---@class OpencodeKeymapEntry
---@field [1] string # Function name
---@field [1] string|function # Action name or callback
---@field [2]? any # Arguments passed to the action or callback
---@field mode? string|string[] # Mode(s) for the keymap
---@field desc? string # Keymap description
---@field nowait? boolean # Execute without waiting for longer mappings
Expand All @@ -169,11 +170,21 @@
---@class OpencodeKeymapInputWindow : table<string, OpencodeKeymapEntry>
---@class OpencodeKeymapOutputWindow : table<string, OpencodeKeymapEntry>

---@alias OpencodeSessionDiffMappings table<string, OpencodeKeymapEntry|false>

---@class OpencodeSessionDiffKeymaps
---@field list OpencodeSessionDiffMappings
---@field messages OpencodeSessionDiffMappings
---@field preview OpencodeSessionDiffMappings
---@field comment OpencodeSessionDiffMappings
---@field message_preview OpencodeSessionDiffMappings
---@field help OpencodeSessionDiffMappings

---@class OpencodeKeymap
---@field editor OpencodeKeymapEditor
---@field input_window OpencodeKeymapInputWindow
---@field output_window OpencodeKeymapOutputWindow
---@field session_diff {list: table<string, OpencodeKeymapEntry|false>, messages: table<string, OpencodeKeymapEntry|false>, preview: table<string, OpencodeKeymapEntry|false>, comment: table<string, OpencodeKeymapEntry|false>, message_preview: table<string, OpencodeKeymapEntry|false>, help: table<string, OpencodeKeymapEntry|false>}
---@field session_diff OpencodeSessionDiffKeymaps
---@field tab_strip_window table<string, OpencodeKeymapEntry>
---@field session_picker OpencodeSessionPickerKeymap
---@field session_tab_picker OpencodeSessionTabPickerKeymap
Expand Down
91 changes: 79 additions & 12 deletions lua/opencode/ui/session_diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local patch = require('opencode.session_patch')
local config = require('opencode.config')
local float_layout = require('opencode.ui.float_layout')
local help = require('opencode.ui.session_diff.help')
local keymaps = require('opencode.keymap')
local icons = require('opencode.ui.icons')
local render = require('opencode.ui.session_diff.render')
local context = require('opencode.context')
Expand Down Expand Up @@ -112,17 +113,16 @@ local function map_actions(buf, scope)
prev_comment = function()
M.jump_comment(-1)
end,
next_file = function()
M.select(1)
end,
prev_file = function()
M.select(-1)
end,
select = M.select,
open_file = M.open_file,
}
for key, entry in pairs(config.keymap.session_diff[scope]) do
if entry ~= false then
vim.keymap.set(entry.mode or 'n', key, actions[entry[1]], {
buffer = buf,
silent = true,
desc = entry.desc,
nowait = entry.nowait,
})
end
end
keymaps.setup_window_keymaps(config.keymap.session_diff[scope], buf, false, actions)
end

local function help_key()
Expand Down Expand Up @@ -216,6 +216,7 @@ function M.toggle_help()

local buf, width, height = help.create(config.keymap.session_diff)
map_actions(buf, 'help')
vim.bo[buf].filetype = 'opencode_diff_help'
view.help_return_win = vim.api.nvim_get_current_win()
view.help_win = vim.api.nvim_open_win(buf, true, {
relative = 'editor',
Expand All @@ -234,7 +235,6 @@ end
local function open_turn_window()
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].bufhidden = 'wipe'
vim.bo[buf].filetype = ''
vim.bo[buf].modifiable = false
local win_config = float_layout.window_configs(nil, false)
win_config.width = math.min(assert(win_config.width), math.floor(vim.o.columns * 0.75)) --[[@as integer]]
Expand All @@ -251,6 +251,7 @@ local function open_turn_window()
vim.wo[view.turn_win].relativenumber = false
vim.wo[view.turn_win].signcolumn = 'no'
map_actions(buf, 'messages')
vim.bo[buf].filetype = 'opencode_diff_messages'
end

local function close_turn_window()
Expand Down Expand Up @@ -388,16 +389,24 @@ scratch = function(lines, filetype, scope)
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].bufhidden = 'wipe'
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].filetype = filetype
vim.bo[buf].modifiable = false
map_actions(buf, scope or 'preview')
if scope == 'list' then
vim.bo[buf].filetype = 'opencode_diff_list'
else
local suffix = scope == 'message_preview' and 'opencode_diff_message_preview' or 'opencode_diff_preview'
vim.bo[buf].filetype = filetype ~= '' and (filetype .. '.' .. suffix) or suffix
vim.bo[buf].syntax = filetype
end
return buf
end

local function set_preview(win, lines, filetype)
vim.api.nvim_win_set_buf(win, scratch(lines, filetype))
vim.api.nvim_win_set_cursor(win, { 1, 0 })
vim.wo[win].signcolumn = 'yes:1'
vim.wo[win].number = true
vim.wo[win].relativenumber = false
end

local function refresh_comments()
Expand Down Expand Up @@ -732,6 +741,63 @@ function M.activate()
vim.api.nvim_win_set_cursor(view.list_win, { row, 0 })
end

---@param win integer
---@return integer
local function working_tree_line(win)
local row = vim.api.nvim_win_get_cursor(win)[1]
if view.sides[win] == 'after' then
return row
end
if view.sides[win] == 'before' then
for index, entry in ipairs(view.patch_map) do
if entry.old == row then
row = index
break
end
end
end
-- Deleted lines and patch headers target the next surviving line, or the last one.
for index = row, #view.patch_map do
local number = view.patch_map[index].new
if number then
return number
end
end
for index = math.min(row - 1, #view.patch_map), 1, -1 do
local number = view.patch_map[index].new
if number then
return number
end
end
return 1
end

function M.open_file()
if not in_view() or #view.files == 0 then
return
end
local index = view.index
local win = vim.api.nvim_get_current_win()
local line = 1
if win == view.list_win then
local node = view.rows[vim.api.nvim_win_get_cursor(view.list_win)[1]]
if not node or not node.file_index then
return
end
index = node.file_index
elseif win == view.preview_win or win == view.right_win then
line = working_tree_line(win)
end
local file = view.files[index]
if vim.fn.filereadable(file.file) == 0 then
vim.notify('Working-tree file not found: ' .. file.file, vim.log.levels.WARN)
return
end
-- Keep snapshot previews intact while editing the current working-tree revision.
vim.cmd('tabedit ' .. vim.fn.fnameescape(file.file))
vim.api.nvim_win_set_cursor(0, { math.min(line, vim.api.nvim_buf_line_count(0)), 0 })
end

function M.select(direction)
if not active() then
return false
Expand Down Expand Up @@ -856,6 +922,7 @@ function M.open(files, session, options)
vim.wo[list_win].relativenumber = false
vim.wo[list_win].signcolumn = 'no'
vim.wo[list_win].cursorline = true
vim.wo[list_win].wrap = false
vim.cmd('rightbelow vsplit')
local preview_win = vim.api.nvim_get_current_win()
vim.api.nvim_set_current_win(list_win)
Expand Down
13 changes: 3 additions & 10 deletions lua/opencode/ui/session_diff/comment_input.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local config = require('opencode.config')
local keymaps = require('opencode.keymap')

local M = {}

Expand Down Expand Up @@ -71,16 +72,8 @@ function M.open(opts)
end
vim.api.nvim_create_autocmd('BufWriteCmd', { buffer = buf, callback = submit })
local actions = { submit_comment = submit, cancel_comment = cancel }
for key, entry in pairs(config.keymap.session_diff.comment) do
if entry ~= false then
vim.keymap.set(entry.mode or 'n', key, actions[entry[1]], {
buffer = buf,
silent = true,
desc = entry.desc,
nowait = entry.nowait,
})
end
end
keymaps.setup_window_keymaps(config.keymap.session_diff.comment, buf, false, actions)
vim.bo[buf].filetype = 'opencode_diff_comment'
vim.cmd('startinsert')
end

Expand Down
11 changes: 8 additions & 3 deletions lua/opencode/ui/session_diff/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ local action_order = {
prev_comment = 13,
submit_comment = 14,
cancel_comment = 15,
next_file = 16,
prev_file = 17,
select = 18,
open_file = 19,
}

---@class OpencodeSessionDiffHelpSection
Expand All @@ -25,13 +29,14 @@ local action_order = {
---@field width integer

---@param title string
---@param mappings table<string, OpencodeKeymapEntry|false>
---@param mappings OpencodeSessionDiffMappings
---@return OpencodeSessionDiffHelpSection
local function section(title, mappings)
local entries = {}
for key, entry in pairs(mappings) do
if entry ~= false then
entries[#entries + 1] = { key = key, action = entry[1], label = entry.desc or entry[1] }
local action = type(entry[1]) == 'string' and entry[1] or 'Custom callback'
entries[#entries + 1] = { key = key, action = action, label = entry.desc or action }
end
end
table.sort(entries, function(a, b)
Expand All @@ -45,7 +50,7 @@ local function section(title, mappings)
return { title = title, entries = entries, width = width }
end

---@param keymaps {list: table<string, OpencodeKeymapEntry|false>, messages: table<string, OpencodeKeymapEntry|false>, preview: table<string, OpencodeKeymapEntry|false>, comment: table<string, OpencodeKeymapEntry|false>, message_preview: table<string, OpencodeKeymapEntry|false>, help: table<string, OpencodeKeymapEntry|false>}
---@param keymaps OpencodeSessionDiffKeymaps
---@return integer, integer, integer
function M.create(keymaps)
local sections = {
Expand Down
Loading
Loading