|
1 | 1 | local state = require('opencode.state') |
2 | | -local output_window = require('opencode.ui.output_window') |
3 | 2 |
|
4 | 3 | local M = {} |
5 | 4 |
|
6 | | --- Module state |
7 | | -local last_line_num = nil |
8 | | -local dirty = false |
9 | | -local current_keymaps = {} |
| 5 | +local namespace = vim.api.nvim_create_namespace('opencode_contextual_actions') |
| 6 | +local augroup = vim.api.nvim_create_augroup('OpenCodeContextualActions', { clear = true }) |
| 7 | +local lifecycles = {} |
10 | 8 |
|
11 | | -local function clear_keymaps(buf) |
12 | | - for key, _ in pairs(current_keymaps) do |
13 | | - vim.keymap.del('n', key, { buffer = buf }) |
| 9 | +local function buffer_mapping(buf, key) |
| 10 | + for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(buf, 'n')) do |
| 11 | + if mapping.lhs == key then |
| 12 | + return mapping |
| 13 | + end |
14 | 14 | end |
15 | | - current_keymaps = {} |
16 | 15 | end |
17 | 16 |
|
18 | | -function M.setup_contextual_actions(windows) |
19 | | - local ns_id = vim.api.nvim_create_namespace('opencode_contextual_actions') |
20 | | - local augroup = vim.api.nvim_create_augroup('OpenCodeContextualActions', { clear = true }) |
21 | | - |
22 | | - vim.api.nvim_create_autocmd('CursorHold', { |
23 | | - group = augroup, |
24 | | - buffer = windows.output_buf, |
25 | | - callback = function() |
26 | | - vim.schedule(function() |
27 | | - local line_num = vim.api.nvim_win_get_cursor(0)[1] |
28 | | - |
29 | | - if not line_num or line_num <= 0 or not state.windows or not state.windows.output_buf then |
30 | | - return |
31 | | - end |
| 17 | +local function restore_mapping(buf, mapping) |
| 18 | + local options = { |
| 19 | + desc = mapping.desc, |
| 20 | + silent = mapping.silent == 1, |
| 21 | + expr = mapping.expr == 1, |
| 22 | + nowait = mapping.nowait == 1, |
| 23 | + noremap = mapping.noremap ~= 0, |
| 24 | + script = mapping.script == 1, |
| 25 | + replace_keycodes = mapping.replace_keycodes == 1, |
| 26 | + } |
| 27 | + if mapping.callback then |
| 28 | + options.callback = mapping.callback |
| 29 | + end |
| 30 | + vim.api.nvim_buf_set_keymap(buf, 'n', mapping.lhs, mapping.callback and '' or mapping.rhs, options) |
| 31 | +end |
32 | 32 |
|
33 | | - line_num = line_num - 1 -- need api-indexing (e.g. 0 based line #), win_get_cursor returns 1 based line # |
| 33 | +local function clear_contextual_actions(buf) |
| 34 | + local lifecycle = lifecycles[buf] |
| 35 | + if not lifecycle then |
| 36 | + return |
| 37 | + end |
34 | 38 |
|
35 | | - local actions = require('opencode.ui.renderer').get_actions_for_line(line_num) |
36 | | - last_line_num = line_num |
| 39 | + if vim.api.nvim_buf_is_valid(buf) then |
| 40 | + vim.api.nvim_buf_clear_namespace(buf, namespace, 0, -1) |
| 41 | + for key, mapping in pairs(lifecycle.saved_mappings) do |
| 42 | + vim.keymap.del('n', key, { buffer = buf }) |
| 43 | + if mapping then |
| 44 | + restore_mapping(buf, mapping) |
| 45 | + end |
| 46 | + end |
| 47 | + end |
| 48 | + lifecycle.saved_mappings = {} |
| 49 | + lifecycle.actions = nil |
| 50 | +end |
37 | 51 |
|
38 | | - vim.api.nvim_buf_clear_namespace(state.windows.output_buf, ns_id, 0, -1) |
39 | | - clear_keymaps(state.windows.output_buf) |
| 52 | +local function ensure_lifecycle(buf) |
| 53 | + local lifecycle = lifecycles[buf] |
| 54 | + if lifecycle then |
| 55 | + return lifecycle |
| 56 | + end |
40 | 57 |
|
41 | | - if actions and #actions > 0 then |
42 | | - dirty = true |
43 | | - M.show_contextual_actions_menu(state.windows.output_buf, actions, ns_id) |
44 | | - end |
45 | | - end) |
| 58 | + lifecycle = { saved_mappings = {}, actions = nil } |
| 59 | + assert(vim.api.nvim_buf_attach(buf, false, { |
| 60 | + on_lines = function() |
| 61 | + clear_contextual_actions(buf) |
46 | 62 | end, |
47 | | - }) |
48 | | - |
49 | | - vim.api.nvim_create_autocmd('CursorMoved', { |
50 | | - group = augroup, |
51 | | - buffer = windows.output_buf, |
52 | | - callback = function() |
53 | | - vim.schedule(function() |
54 | | - if not output_window.mounted() then |
55 | | - return |
56 | | - end |
57 | | - ---@cast state.windows { output_buf: integer} |
58 | | - local line_num = vim.api.nvim_win_get_cursor(0)[1] |
59 | | - if last_line_num == line_num and not dirty then |
60 | | - return |
61 | | - end |
62 | | - vim.api.nvim_buf_clear_namespace(state.windows.output_buf, ns_id, 0, -1) |
63 | | - end) |
| 63 | + on_reload = function() |
| 64 | + clear_contextual_actions(buf) |
64 | 65 | end, |
65 | | - }) |
66 | | - |
67 | | - vim.api.nvim_create_autocmd({ 'BufLeave', 'BufDelete', 'BufHidden' }, { |
68 | | - group = augroup, |
69 | | - buffer = windows.output_buf, |
70 | | - callback = function() |
71 | | - if state.windows and state.windows.output_buf then |
72 | | - vim.api.nvim_buf_clear_namespace(state.windows.output_buf, ns_id, 0, -1) |
73 | | - clear_keymaps(state.windows.output_buf) |
74 | | - end |
75 | | - last_line_num = nil |
76 | | - dirty = false |
| 66 | + on_detach = function() |
| 67 | + clear_contextual_actions(buf) |
| 68 | + lifecycles[buf] = nil |
77 | 69 | end, |
78 | | - }) |
| 70 | + })) |
| 71 | + lifecycles[buf] = lifecycle |
| 72 | + return lifecycle |
79 | 73 | end |
80 | 74 |
|
81 | | -function M.show_contextual_actions_menu(buf, actions, ns_id) |
82 | | - clear_keymaps(buf) |
| 75 | +function M.show_contextual_actions_menu(buf, actions) |
| 76 | + local lifecycle = ensure_lifecycle(buf) |
| 77 | + actions = actions and #actions > 0 and actions or nil |
| 78 | + |
| 79 | + if vim.deep_equal(lifecycle.actions, actions) then |
| 80 | + return |
| 81 | + end |
| 82 | + |
| 83 | + clear_contextual_actions(buf) |
| 84 | + if not actions then |
| 85 | + return |
| 86 | + end |
| 87 | + lifecycle.actions = actions |
83 | 88 |
|
84 | 89 | for _, action in ipairs(actions) do |
85 | | - ---@type OutputExtmark |
86 | | - local mark = { |
| 90 | + vim.api.nvim_buf_set_extmark(buf, namespace, action.display_line, 0, { |
87 | 91 | virt_text = { { '⋮ ' .. action.text .. ' ', 'OpencodeContextualActions' } }, |
88 | 92 | virt_text_pos = 'right_align', |
89 | 93 | hl_mode = 'combine', |
90 | | - } |
| 94 | + }) |
91 | 95 |
|
92 | | - vim.api.nvim_buf_set_extmark(buf, ns_id, action.display_line, 0, mark --[[@as vim.api.keyset.set_extmark]]) |
93 | | - end |
94 | | - -- Setup key mappings for actions |
95 | | - for _, action in ipairs(actions) do |
96 | | - if action.key then |
97 | | - current_keymaps[action.key] = true |
| 96 | + if action.key and lifecycle.saved_mappings[action.key] == nil then |
| 97 | + lifecycle.saved_mappings[action.key] = buffer_mapping(buf, action.key) or false |
98 | 98 | vim.keymap.set('n', action.key, function() |
| 99 | + if lifecycles[buf] ~= lifecycle or not vim.tbl_contains(lifecycle.actions or {}, action) then |
| 100 | + return |
| 101 | + end |
| 102 | + |
| 103 | + clear_contextual_actions(buf) |
99 | 104 | if action.type and action.args then |
100 | | - vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1) |
101 | | - clear_keymaps(buf) |
102 | | - local api = require('opencode.api') |
103 | | - api[action.type](unpack(action.args)) |
| 105 | + require('opencode.api')[action.type](unpack(action.args)) |
104 | 106 | end |
105 | 107 | end, { buffer = buf, silent = true, desc = action.text }) |
106 | 108 | end |
107 | 109 | end |
108 | 110 | end |
109 | 111 |
|
| 112 | +local function refresh_contextual_actions(buf) |
| 113 | + local lifecycle = lifecycles[buf] |
| 114 | + if not lifecycle then |
| 115 | + return |
| 116 | + end |
| 117 | + |
| 118 | + if not state.windows or state.windows.output_buf ~= buf or vim.api.nvim_get_current_buf() ~= buf then |
| 119 | + clear_contextual_actions(buf) |
| 120 | + return |
| 121 | + end |
| 122 | + |
| 123 | + local line = vim.api.nvim_win_get_cursor(0)[1] - 1 |
| 124 | + M.show_contextual_actions_menu(buf, require('opencode.ui.renderer').get_actions_for_line(line)) |
| 125 | +end |
| 126 | + |
| 127 | +function M.setup_contextual_actions(windows) |
| 128 | + ensure_lifecycle(windows.output_buf) |
| 129 | + refresh_contextual_actions(windows.output_buf) |
| 130 | +end |
| 131 | + |
| 132 | +vim.api.nvim_create_autocmd({ 'CursorHold', 'CursorMoved', 'BufEnter', 'WinEnter' }, { |
| 133 | + group = augroup, |
| 134 | + callback = function(event) |
| 135 | + refresh_contextual_actions(event.buf) |
| 136 | + end, |
| 137 | +}) |
| 138 | + |
| 139 | +vim.api.nvim_create_autocmd({ 'BufLeave', 'BufDelete', 'BufHidden' }, { |
| 140 | + group = augroup, |
| 141 | + callback = function(event) |
| 142 | + clear_contextual_actions(event.buf) |
| 143 | + end, |
| 144 | +}) |
| 145 | + |
110 | 146 | return M |
0 commit comments