Neovim plugin improving access to clipboard history (mirror)
at main 296 lines 9.6 kB view raw
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