Neovim plugin improving access to clipboard history (mirror)
1local M = {}
2
3local state = require("yankbank.state")
4
5-- default plugin keymaps
6local default_keymaps = {
7 navigation_next = "j",
8 navigation_prev = "k",
9 paste = "<CR>",
10 paste_back = "P",
11 yank = "yy",
12 close = { "<Esc>", "<C-c>", "q" },
13}
14
15-- define default yank register
16local default_registers = {
17 yank_register = "+",
18}
19
20function M.setup()
21 local opts = state.get_opts()
22 -- merge default and options keymap tables
23 opts.keymaps =
24 vim.tbl_deep_extend("force", default_keymaps, opts.keymaps or {})
25 -- merge default and options register tables
26 opts.registers =
27 vim.tbl_deep_extend("force", default_registers, opts.registers or {})
28
29 -- check table for number behavior option (prefix or jump, default to prefix)
30 opts.num_behavior = opts.num_behavior or "prefix"
31
32 state.set_opts(opts)
33end
34
35--- reformat yanks table for popup
36---@return table, table
37local function get_display_lines()
38 local display_lines = {}
39 local line_yank_map = {}
40 local yank_num = 0
41
42 local yanks = state.get_yanks()
43 local opts = state.get_opts()
44
45 -- calculate the maximum width needed for the yank numbers
46 local max_digits = #tostring(#yanks)
47
48 -- assumes yanks is table of strings
49 for i, yank in ipairs(yanks) do
50 yank_num = yank_num + 1
51
52 local yank_lines = yank
53 if type(yank) == "string" then
54 -- remove trailing newlines
55 yank = yank:gsub("\n$", "")
56 yank_lines = vim.split(yank, "\n", { plain = true })
57 end
58
59 local leading_space, leading_space_length
60
61 -- determine the number of leading whitespaces on the first line
62 if #yank_lines > 0 then
63 leading_space = yank_lines[1]:match("^(%s*)")
64 leading_space_length = #leading_space
65 end
66
67 for j, line in ipairs(yank_lines) do
68 if j == 1 then
69 -- Format the line number with uniform spacing
70 local lineNumber =
71 string.format("%" .. max_digits .. "d: ", yank_num)
72 line = line:sub(leading_space_length + 1)
73 table.insert(display_lines, lineNumber .. line)
74 else
75 -- Remove the same amount of leading whitespace as on the first line
76 line = line:sub(leading_space_length + 1)
77 -- Use spaces equal to the line number's reserved space to align subsequent lines
78 table.insert(
79 display_lines,
80 string.rep(" ", max_digits + 2) .. line
81 )
82 end
83 table.insert(line_yank_map, i)
84 end
85
86 if i < #yanks then
87 -- Add a visual separator between yanks, aligned with the yank content
88 if opts.sep ~= "" then
89 table.insert(
90 display_lines,
91 string.rep(" ", max_digits + 2) .. opts.sep
92 )
93 end
94 table.insert(line_yank_map, false)
95 end
96 end
97
98 return display_lines, line_yank_map
99end
100
101--- Container class for YankBank buffer related variables
102---@class YankBankBufData
103---@field bufnr integer
104---@field display_lines table
105---@field line_yank_map table
106---@field win_id integer
107
108---create new buffer and reformat yank table for ui
109---@return YankBankBufData?
110function M.create_and_fill_buffer()
111 -- stop if yanks or register types table is empty
112 local yanks = state.get_yanks()
113 local reg_types = state.get_reg_types()
114 if #yanks == 0 or #reg_types == 0 then
115 print("No yanks to show.")
116 return nil
117 end
118
119 -- create new buffer
120 local bufnr = vim.api.nvim_create_buf(false, true)
121
122 -- set buffer type same as current window for syntax highlighting
123 local current_filetype = vim.bo.filetype
124 vim.api.nvim_set_option_value("filetype", current_filetype, { buf = bufnr })
125
126 local display_lines, line_yank_map = get_display_lines()
127
128 -- replace current buffer contents with updated table
129 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, display_lines)
130
131 ---@type YankBankBufData
132 return {
133 bufnr = bufnr,
134 display_lines = display_lines,
135 line_yank_map = line_yank_map,
136 win_id = -1,
137 }
138end
139
140---Calculate size and create popup window from bufnr
141---@param b YankBankBufData
142---@return integer
143function M.open_window(b)
144 -- set maximum window width based on number of lines
145 local max_width = 0
146 if b.display_lines and #b.display_lines > 0 then
147 for _, line in ipairs(b.display_lines) do
148 max_width = math.max(max_width, #line)
149 end
150 else
151 max_width = vim.api.nvim_get_option_value("columns", {})
152 end
153
154 -- define buffer window width and height based on number of columns
155 -- FIX: long enough entries will cause window to go below end of screen
156 -- FIX: wrapping long lines will cause entries below to not show in menu (requires scrolling to see)
157 local width =
158 math.min(max_width, vim.api.nvim_get_option_value("columns", {}) - 4)
159 local height = math.min(
160 b.display_lines and #b.display_lines or 1,
161 vim.api.nvim_get_option_value("lines", {}) - 10
162 )
163
164 -- open window
165 local win_id = vim.api.nvim_open_win(b.bufnr, true, {
166 relative = "editor",
167 width = width,
168 height = height,
169 col = math.floor(
170 (vim.api.nvim_get_option_value("columns", {}) - width) / 2
171 ),
172 row = math.floor(
173 (vim.api.nvim_get_option_value("lines", {}) - height) / 2
174 ),
175 border = "rounded",
176 style = "minimal",
177 })
178
179 -- Highlight current line
180 vim.api.nvim_set_option_value("cursorline", true, { win = win_id })
181
182 return win_id
183end
184
185--- Set key mappings for the popup window
186---@param b YankBankBufData
187function M.set_keymaps(b)
188 -- key mappings for selection and closing the popup
189 local map_opts = { noremap = true, silent = true, buffer = b.bufnr }
190 local opts = state.get_opts()
191
192 local helpers = require("yankbank.helpers")
193
194 -- popup buffer navigation binds
195 if opts.num_behavior == "prefix" then
196 vim.keymap.set("n", opts.keymaps.navigation_next, function()
197 local count = vim.v.count1 > 0 and vim.v.count1 or 1
198 helpers.next_numbered_item(count)
199 return ""
200 end, { noremap = true, silent = true, buffer = b.bufnr })
201 vim.keymap.set("n", opts.keymaps.navigation_prev, function()
202 local count = vim.v.count1 > 0 and vim.v.count1 or 1
203 helpers.prev_numbered_item(count)
204 return ""
205 end, map_opts)
206 else
207 vim.keymap.set(
208 "n",
209 opts.keymaps.navigation_next,
210 helpers.next_numbered_item,
211 map_opts
212 )
213 vim.keymap.set(
214 "n",
215 opts.keymaps.navigation_prev,
216 helpers.prev_numbered_item,
217 map_opts
218 )
219 end
220
221 -- map number keys to jump to entry if num_behavior is 'jump'
222 if opts.num_behavior == "jump" then
223 for i = 1, opts.max_entries do
224 vim.keymap.set("n", tostring(i), function()
225 local target_line = nil
226 for line_num, yank_num in pairs(b.line_yank_map) do
227 if yank_num == i then
228 target_line = line_num
229 break
230 end
231 end
232 if target_line then
233 vim.api.nvim_win_set_cursor(b.win_id, { target_line, 0 })
234 end
235 end, map_opts)
236 end
237 end
238
239 -- bind paste behavior
240 vim.keymap.set("n", opts.keymaps.paste, function()
241 local cursor = vim.api.nvim_win_get_cursor(b.win_id)[1]
242 -- use the mapping to find the original yank
243 local yankIndex = b.line_yank_map[cursor]
244 if yankIndex then
245 -- close window upon selection
246 vim.api.nvim_win_close(b.win_id, true)
247 helpers.smart_paste(
248 state.get_yanks()[yankIndex],
249 state.get_reg_types()[yankIndex],
250 true
251 )
252 else
253 print("Error: Invalid selection")
254 end
255 end, map_opts)
256 -- paste backwards
257 vim.keymap.set("n", opts.keymaps.paste_back, function()
258 local cursor = vim.api.nvim_win_get_cursor(b.win_id)[1]
259 -- use the mapping to find the original yank
260 local yankIndex = b.line_yank_map[cursor]
261 if yankIndex then
262 -- close window upon selection
263 vim.api.nvim_win_close(b.win_id, true)
264 helpers.smart_paste(
265 state.get_yanks()[yankIndex],
266 state.get_reg_types()[yankIndex],
267 false
268 )
269 else
270 print("Error: Invalid selection")
271 end
272 end, map_opts)
273
274 -- bind yank behavior
275 vim.keymap.set("n", opts.keymaps.yank, function()
276 local cursor = vim.api.nvim_win_get_cursor(b.win_id)[1]
277 local yankIndex = b.line_yank_map[cursor]
278 if yankIndex then
279 vim.fn.setreg(
280 opts.registers.yank_register,
281 state.get_yanks()[yankIndex]
282 )
283 vim.api.nvim_win_close(b.win_id, true)
284 end
285 end, map_opts)
286
287 -- close popup keybinds
288 -- REFACTOR: check if close keybind is string, handle differently
289 for _, map in ipairs(opts.keymaps.close) do
290 vim.keymap.set("n", map, function()
291 vim.api.nvim_win_close(b.win_id, true)
292 end, map_opts)
293 end
294end
295
296return M