Neovim plugin improving access to clipboard history (mirror)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial Commit

Patrick Dewey d4d8141e

+355
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2024 Patrick 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+41
README.md
··· 1 + # YankBank 2 + A Neovim plugin for keeping track of more recent yanks and deletions and exposing them in a quick to access menu. 3 + 4 + ## What it Does 5 + <!-- TODO: screenshots --> 6 + <!-- TODO: talk about how the menu populates--> 7 + TODO: 8 + 9 + 10 + ## Installation and Setup 11 + 12 + Lazy: 13 + ```lua 14 + { 15 + "ptdewey/yankbank-nvim", 16 + config = function() 17 + require('yankbank').setup() 18 + end, 19 + } 20 + ``` 21 + 22 + Packer: 23 + ```lua 24 + use { 25 + 'ptdewey/yankbank-nvim', 26 + config = function() 27 + require('yankbank').setup() 28 + end, 29 + } 30 + ``` 31 + 32 + ## Usage 33 + 34 + The popup menu can be opened with the command:`:YankBank`, an entry is pasted at the current cursor position by hitting enter, and the menu can be closed by hitting escape, ctrl-c, or q. 35 + 36 + I would personally also recommend setting a keybind to open the menu. 37 + ```lua 38 + -- map to '<leader>y' 39 + vim.keymap.set("n", "<leader>y", ":YankBank<CR>", { noremap = true }) 40 + ``` 41 +
+44
lua/yankbank/clipboard.lua
··· 1 + -- clipboard.lua 2 + local M = {} 3 + 4 + -- Function to add yanked text to table 5 + function M.add_yank(yanks, text, max_entries) 6 + -- avoid adding empty strings 7 + if text ~= "" then 8 + table.insert(yanks, 1, text) 9 + 10 + if #yanks > max_entries then 11 + table.remove(yanks) 12 + end 13 + end 14 + end 15 + 16 + -- autocommand to listen for yank events 17 + function M.setup_yank_autocmd(yanks, max_entries) 18 + vim.api.nvim_create_autocmd("TextYankPost", { 19 + callback = function() 20 + -- TODO: this function can be expanded to incorporate other registers 21 + -- - use vim.v.event.regname and an allowlist 22 + -- if reg_type == "y" or reg_type == "d" then 23 + -- local yanked_text = vim.fn.getreg('"') 24 + 25 + -- get register information 26 + local reg_type = vim.v.event.operator 27 + local rn = vim.v.event.regname 28 + 29 + -- check changes wwere made to default register 30 + if vim.v.event.regname == '' then 31 + local yanked_text = vim.fn.getreg(rn) 32 + 33 + -- don't track single character deletions 34 + if #yanked_text <= 1 and reg_type ~= "y" then 35 + return 36 + end 37 + 38 + M.add_yank(yanks, yanked_text, max_entries) 39 + end 40 + end, 41 + }) 42 + end 43 + 44 + return M
+53
lua/yankbank/data.lua
··· 1 + -- data.lua 2 + local M = {} 3 + 4 + -- reformat yanks table for popup 5 + function M.get_display_lines(yanks) 6 + local display_lines = {} 7 + local line_yank_map = {} 8 + local yank_num = 0 9 + 10 + -- calculate the maximum width needed for the yank numbers 11 + local max_digits = #tostring(#yanks) 12 + 13 + for i, yank in ipairs(yanks) do 14 + yank_num = yank_num + 1 15 + 16 + -- remove trailing newlines 17 + yank = yank:gsub("\n$", "") 18 + local yank_lines = vim.split(yank, "\n", { plain= true }) 19 + local leading_space, leading_space_length 20 + 21 + -- determine the number of leading whitespaces on the first line 22 + if #yank_lines > 0 then 23 + leading_space = yank_lines[1]:match("^(%s*)") 24 + leading_space_length = #leading_space 25 + end 26 + 27 + for j, line in ipairs(yank_lines) do 28 + if j == 1 then 29 + -- Format the line number with uniform spacing 30 + local lineNumber = string.format("%" .. max_digits .. "d: ", yank_num) 31 + line = line:sub(leading_space_length + 1) 32 + table.insert(display_lines, lineNumber .. line) 33 + else 34 + -- Remove the same amount of leading whitespace as on the first line 35 + line = line:sub(leading_space_length + 1) 36 + -- Use spaces equal to the line number's reserved space to align subsequent lines 37 + table.insert(display_lines, string.rep(" ", max_digits + 2) .. line) 38 + end 39 + table.insert(line_yank_map, i) 40 + end 41 + 42 + -- Add a visual separator between yanks, aligned with the yank content 43 + -- TODO: allow turning off/on in plugin setup 44 + if i < #yanks then 45 + table.insert(display_lines, string.rep(" ", max_digits + 2) .. "------") 46 + table.insert(line_yank_map, false) 47 + end 48 + end 49 + 50 + return display_lines, line_yank_map 51 + end 52 + 53 + return M
+45
lua/yankbank/helpers.lua
··· 1 + -- helpers.lua 2 + local M = {} 3 + 4 + -- navigate to the next numbered item 5 + function M.next_numbered_item() 6 + local current_line = vim.api.nvim_win_get_cursor(0)[1] 7 + local total_lines = vim.api.nvim_buf_line_count(0) 8 + for i = current_line + 1, total_lines do 9 + local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] 10 + -- search for the correct line start 11 + if line:match("^%s*%d+:") then 12 + vim.api.nvim_win_set_cursor(0, {i, 0}) 13 + break 14 + end 15 + end 16 + end 17 + 18 + 19 + -- navigate to the previous numbered item 20 + function M.prev_numbered_item() 21 + local current_line = vim.api.nvim_win_get_cursor(0)[1] 22 + for i = current_line - 1, 1, -1 do 23 + local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] 24 + -- search for the correct line start 25 + if line:match("^%s*%d+:") then 26 + vim.api.nvim_win_set_cursor(0, {i, 0}) 27 + break 28 + end 29 + end 30 + end 31 + 32 + -- customized paste function that functions more like 'p' 33 + function M.smart_paste(text) 34 + -- determine if the text should be treated as line-wise based on its ending 35 + if text:sub(-1) == '\n' then 36 + -- line-wise 37 + vim.cmd("normal! o") 38 + vim.api.nvim_paste(text, false, -1) 39 + else 40 + -- character-wise 41 + vim.api.nvim_paste(text, false, -1) 42 + end 43 + end 44 + 45 + return M
+40
lua/yankbank/init.lua
··· 1 + -- init.lua 2 + local M = {} 3 + 4 + -- local imports 5 + local menu = require("yankbank.menu") 6 + local clipboard = require("yankbank.clipboard") 7 + 8 + -- initialize yanks tables 9 + local yanks = {} 10 + local max_entries = 10 11 + 12 + -- wrapper function for main plugin functionality 13 + local function show_yank_bank() 14 + -- TODO: update max entries with passed in options 15 + local bufnr, display_lines, line_yank_map = menu.create_and_fill_buffer(yanks, max_entries) 16 + local win_id = menu.open_window(bufnr, display_lines) 17 + menu.set_keymaps(win_id, bufnr, yanks, line_yank_map) 18 + end 19 + 20 + -- plugin setup 21 + function M.setup(opts) 22 + local o = {} 23 + 24 + -- parse opts 25 + if opts ~= nil then 26 + o.max_entries = opts.max_entries or max_entries 27 + else 28 + o.max_entries = max_entries 29 + end 30 + 31 + -- create clipboard autocmds 32 + clipboard.setup_yank_autocmd(yanks, o.max_entries) 33 + 34 + -- Create user command 35 + -- TODO: allow params (i.e. keymaps/max_entries/separator) 36 + vim.api.nvim_create_user_command("YankBank", show_yank_bank, 37 + { desc = "Show Recent Yanks" }) 38 + end 39 + 40 + return M
+111
lua/yankbank/menu.lua
··· 1 + -- menu.lua 2 + local M = {} 3 + 4 + -- import clipboard functions 5 + local clipboard = require("yankbank.clipboard") 6 + local data = require("yankbank.data") 7 + local helpers = require("yankbank.helpers") 8 + 9 + -- create new buffer and reformat yank table for ui 10 + function M.create_and_fill_buffer(yanks, max_entries) 11 + -- check the content of the system clipboard register 12 + -- TODO: this could be replaced with some sort of polling of the + register 13 + local text = vim.fn.getreg('+') 14 + local most_recent_yank = yanks[1] or "" 15 + if text ~= most_recent_yank then 16 + clipboard.add_yank(yanks, text, max_entries) 17 + end 18 + 19 + -- stop if yank table is empty 20 + if #yanks == 0 then 21 + print("No yanks to show.") 22 + return 23 + end 24 + 25 + -- create new buffer 26 + local bufnr = vim.api.nvim_create_buf(false, true) 27 + 28 + -- set buffer type same as current window for syntax highlighting 29 + local current_filetype = vim.bo.filetype 30 + vim.api.nvim_buf_set_option(bufnr, 'filetype', current_filetype) 31 + 32 + local display_lines, line_yank_map = data.get_display_lines(yanks) 33 + 34 + -- replace current buffer contents with updated table 35 + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, display_lines) 36 + 37 + return bufnr, display_lines, line_yank_map 38 + end 39 + 40 + -- Calculate size and create popup window from bufnr 41 + function M.open_window(bufnr, display_lines) 42 + -- set maximum window width based on number of lines 43 + local max_width = 0 44 + for _, line in ipairs(display_lines) do 45 + max_width = math.max(max_width, #line) 46 + end 47 + 48 + -- define buffer window width and height based on number of columns 49 + local width = math.min(max_width + 4, vim.api.nvim_get_option("columns") - 50) 50 + local height = math.min(#display_lines, vim.api.nvim_get_option("lines") - 4) 51 + 52 + -- open window 53 + local win_id = vim.api.nvim_open_win(bufnr, true, { 54 + relative = "editor", 55 + width = width, 56 + height = height, 57 + col = math.floor((vim.api.nvim_get_option("columns") - width) / 2 - 1), 58 + row = math.floor((vim.api.nvim_get_option("lines") - height) / 2) - 1, 59 + border = "rounded", 60 + style = "minimal", 61 + }) 62 + 63 + -- Highlight current line 64 + vim.api.nvim_win_set_option(win_id, 'cursorline', true) 65 + 66 + return win_id 67 + end 68 + 69 + -- Set key mappings for the popup window 70 + -- TODO: configurable options (take in inside setup function) 71 + function M.set_keymaps(win_id, bufnr, yanks, line_yank_map) 72 + -- Key mappings for selection and closing the popup 73 + local map_opts = { noremap = true, silent = true, buffer = bufnr } 74 + 75 + -- popup buffer navigation binds 76 + vim.keymap.set('n', 'j', helpers.next_numbered_item, 77 + { noremap = true, silent = true, buffer = bufnr }) 78 + vim.keymap.set('n', 'k', helpers.prev_numbered_item, 79 + { noremap = true, silent = true, buffer = bufnr }) 80 + 81 + -- bind paste behavior to enter 82 + vim.keymap.set('n', '<CR>', function() 83 + local cursor = vim.api.nvim_win_get_cursor(win_id)[1] 84 + -- use the mapping to find the original yank 85 + local yankIndex = line_yank_map[cursor] 86 + if yankIndex then 87 + -- retrieve the full yank, including all lines 88 + local text = yanks[yankIndex] 89 + 90 + -- close window upon selection 91 + vim.api.nvim_win_close(win_id, true) 92 + 93 + -- call the custom paste function with adjusted indentation 94 + print(vim.fn.getregtype()) 95 + -- vim.api.nvim_paste(text, false, -1) 96 + helpers.smart_paste(text) 97 + else 98 + print("Error: Invalid selection") 99 + end 100 + end, { buffer = bufnr }) 101 + 102 + -- close popup keybinds 103 + local close_maps = { "<Esc>", "<C-c>", "q" } 104 + for _, map in ipairs(close_maps) do 105 + vim.keymap.set('n', map, function() 106 + vim.api.nvim_win_close(win_id, true) 107 + end, map_opts) 108 + end 109 + end 110 + 111 + return M