Skip to content

Commit 3f3d7a3

Browse files
JMarkinsindrets
andauthored
feat(help): Help panel popup (#257)
Co-authored-by: Sindre T. Strøm <sindrets@gmail.com> Resolves #168
1 parent b97bee2 commit 3f3d7a3

File tree

5 files changed

+223
-25
lines changed

5 files changed

+223
-25
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ require("diffview").setup({
318318
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
319319
{ "n", "q", actions.close, { desc = "Close the panel" } },
320320
},
321+
help_panel = {
322+
{ "n", "q", actions.close, { desc = "Close help menu" } },
323+
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
324+
},
321325
},
322326
})
323327
```

doc/diffview_defaults.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ require("diffview").setup({
172172
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
173173
{ "n", "q", actions.close, { desc = "Close the panel" } },
174174
},
175+
help_panel = {
176+
{ "n", "q", actions.close, { desc = "Close help menu" } },
177+
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
178+
},
175179
},
176180
})
177181

lua/diffview/actions.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local lazy = require("diffview.lazy")
22

33
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
4+
local HelpPanel = lazy.access("diffview.ui.panels.help_panel", "HelpPanel") ---@type HelpPanel|LazyModule
45
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
56
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
67
local vcs = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
@@ -465,6 +466,17 @@ function M.cycle_layout()
465466
end
466467
end
467468

469+
function M.help(conf_name)
470+
return function()
471+
local view = lib.get_current_view()
472+
473+
if view then
474+
local help_panel = HelpPanel(view, conf_name) --[[@as HelpPanel ]]
475+
help_panel:focus()
476+
end
477+
end
478+
end
479+
468480
local action_names = {
469481
"close",
470482
"close_all_folds",

lua/diffview/config.lua

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ M.defaults = {
9494
win_opts = {}
9595
},
9696
},
97+
help_panel = {
98+
win_config = {
99+
win_opts = {}
100+
},
101+
},
97102
default_args = {
98103
DiffviewOpen = {},
99104
DiffviewFileHistory = {},
@@ -120,6 +125,7 @@ M.defaults = {
120125
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
121126
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
122127
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
128+
{ "n", "g?", actions.help("view"), { desc = "Open the help panel" } },
123129
},
124130
diff1 = { --[[ Mappings in single window diff layouts ]] },
125131
diff2 = { --[[ Mappings in 2-way diff layouts ]] },
@@ -162,35 +168,42 @@ M.defaults = {
162168
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
163169
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
164170
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
171+
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
165172
},
166173
file_history_panel = {
167-
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
168-
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
169-
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
170-
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
171-
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
172-
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
173-
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
174-
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
175-
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry." } },
176-
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry." } },
177-
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry." } },
178-
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry." } },
179-
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry." } },
180-
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
181-
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
182-
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
183-
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
184-
{ "n", "gf", actions.goto_file, { desc = "Open the file in a new split in the previous tabpage" } },
185-
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
186-
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
187-
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
188-
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
189-
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
174+
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
175+
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
176+
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
177+
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
178+
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
179+
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
180+
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
181+
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
182+
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry." } },
183+
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry." } },
184+
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry." } },
185+
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry." } },
186+
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry." } },
187+
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
188+
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
189+
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
190+
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
191+
{ "n", "gf", actions.goto_file, { desc = "Open the file in a new split in the previous tabpage" } },
192+
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
193+
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
194+
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
195+
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
196+
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
197+
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
190198
},
191199
option_panel = {
192-
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
193-
{ "n", "q", actions.close, { desc = "Close the panel" } },
200+
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
201+
{ "n", "q", actions.close, { desc = "Close the panel" } },
202+
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
203+
},
204+
help_panel = {
205+
{ "n", "q", actions.close, { desc = "Close help menu" } },
206+
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
194207
},
195208
},
196209
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
local Panel = require("diffview.ui.panel").Panel
2+
local get_user_config = require("diffview.config").get_config
3+
local oop = require("diffview.oop")
4+
local utils = require("diffview.utils")
5+
6+
local api = vim.api
7+
8+
local M = {}
9+
10+
---@class HelpPanel : Panel
11+
---@field keymap_name string
12+
---@field lines string[]
13+
---@field maps table[]
14+
local HelpPanel = oop.create_class("HelpPanel", Panel)
15+
16+
HelpPanel.winopts = vim.tbl_extend("force", Panel.winopts, {
17+
wrap = false,
18+
breakindent = true,
19+
signcolumn = "no",
20+
})
21+
22+
HelpPanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
23+
buftype = "nofile",
24+
})
25+
26+
HelpPanel.default_type = "float"
27+
28+
---@class HelpPanelSpec
29+
---@field parent StandardView
30+
---@field config PanelConfig
31+
---@field name string
32+
33+
---@param parent StandardView
34+
---@param keymap_name string
35+
---@param opt HelpPanelSpec
36+
function HelpPanel:init(parent, keymap_name, opt)
37+
opt = opt or {}
38+
HelpPanel:super().init(self, {
39+
bufname = opt.name,
40+
config = opt.config or get_user_config().help_panel.win_config,
41+
})
42+
43+
self.parent = parent
44+
self.keymap_name = keymap_name
45+
self.lines = {}
46+
47+
self:on_autocmd("BufWinEnter", {
48+
callback = function()
49+
vim.bo[self.bufid].bufhidden = "wipe"
50+
end,
51+
})
52+
53+
self:on_autocmd("WinLeave", {
54+
callback = function()
55+
pcall(self.close, self)
56+
end,
57+
})
58+
59+
parent.emitter:on("close", function(e)
60+
if self:is_focused() then
61+
pcall(self.close, self)
62+
e:stop_propagation()
63+
end
64+
end)
65+
end
66+
67+
function HelpPanel:apply_cmd()
68+
local row, _ = unpack(vim.api.nvim_win_get_cursor(0))
69+
local mapping = self.maps[row-2]
70+
local last_winid = vim.fn.win_getid(vim.fn.winnr("#"))
71+
72+
if mapping then
73+
api.nvim_win_call(last_winid, function()
74+
api.nvim_feedkeys(utils.t(mapping[2]), "m", false)
75+
end)
76+
77+
pcall(self.close, self)
78+
end
79+
end
80+
81+
function HelpPanel:init_buffer()
82+
HelpPanel:super().init_buffer(self)
83+
local conf = get_user_config().keymaps
84+
local default_opt = { silent = true, nowait = true, buffer = self.bufid }
85+
86+
for _, mapping in ipairs(conf.help_panel) do
87+
local map_opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
88+
vim.keymap.set(mapping[1], mapping[2], mapping[3], map_opt)
89+
end
90+
91+
vim.keymap.set("n", "<cr>", function()
92+
self:apply_cmd()
93+
end, default_opt)
94+
end
95+
96+
function HelpPanel:update_components()
97+
local keymaps = get_user_config().keymaps
98+
local maps = keymaps[self.keymap_name]
99+
100+
if not maps then
101+
utils.err(("Unknown keymap group '%s'!"):format(self.keymap_name))
102+
else
103+
maps = vim.tbl_map(function(v)
104+
local desc = v[4] and v[4].desc
105+
106+
if not desc then
107+
if type(v[3]) == "string" then
108+
desc = v[3]
109+
elseif type(v[3]) == "function" then
110+
local info = debug.getinfo(v[3], "S")
111+
desc = ("<Lua @ %s:%d>"):format(info.short_src, info.linedefined)
112+
end
113+
end
114+
115+
return utils.vec_join(v, desc)
116+
end, maps)
117+
-- Sort mappings by description
118+
table.sort(maps, function(a, b)
119+
a, b = a[5], b[5]
120+
-- Ensure lua functions are sorted last
121+
if a:match("^<Lua") then a = "~" .. a end
122+
if b:match("^<Lua") then b = "~" .. b end
123+
return a < b
124+
end)
125+
126+
local lines = { "" }
127+
local max_width = 0
128+
129+
for _, mapping in ipairs(maps) do
130+
local txt = string.format("%14s -> %s", mapping[2], mapping[5])
131+
132+
max_width = math.max(max_width, #txt)
133+
table.insert(lines, txt)
134+
end
135+
136+
local height = #lines + 1
137+
local width = max_width + 2
138+
local title_line = ("Keymaps for '%s' — <cr> to use"):format(self.keymap_name)
139+
title_line = string.rep(" ", math.floor(width * 0.5 - #title_line * 0.5) - 1) .. title_line
140+
table.insert(lines, 1, title_line)
141+
142+
self.maps = maps
143+
self.lines = lines
144+
145+
self.config_producer = function()
146+
local c = vim.deepcopy(Panel.default_config_float)
147+
local viewport_width = vim.o.columns
148+
local viewport_height = vim.o.lines
149+
c.col = math.floor(viewport_width * 0.5 - width * 0.5)
150+
c.row = math.floor(viewport_height * 0.5 - height * 0.5)
151+
c.width = width
152+
c.height = height
153+
154+
return c
155+
end
156+
end
157+
end
158+
159+
function HelpPanel:render()
160+
self.render_data:clear()
161+
self.render_data.lines = utils.vec_slice(self.lines or {})
162+
end
163+
164+
M.HelpPanel = HelpPanel
165+
return M

0 commit comments

Comments
 (0)