···11+MIT License
22+33+Copyright (c) 2024 Patrick
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+41
README.md
···11+# YankBank
22+A Neovim plugin for keeping track of more recent yanks and deletions and exposing them in a quick to access menu.
33+44+## What it Does
55+<!-- TODO: screenshots -->
66+<!-- TODO: talk about how the menu populates-->
77+TODO:
88+99+1010+## Installation and Setup
1111+1212+Lazy:
1313+```lua
1414+{
1515+ "ptdewey/yankbank-nvim",
1616+ config = function()
1717+ require('yankbank').setup()
1818+ end,
1919+}
2020+```
2121+2222+Packer:
2323+```lua
2424+use {
2525+ 'ptdewey/yankbank-nvim',
2626+ config = function()
2727+ require('yankbank').setup()
2828+ end,
2929+}
3030+```
3131+3232+## Usage
3333+3434+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.
3535+3636+I would personally also recommend setting a keybind to open the menu.
3737+```lua
3838+-- map to '<leader>y'
3939+vim.keymap.set("n", "<leader>y", ":YankBank<CR>", { noremap = true })
4040+```
4141+
+44
lua/yankbank/clipboard.lua
···11+-- clipboard.lua
22+local M = {}
33+44+-- Function to add yanked text to table
55+function M.add_yank(yanks, text, max_entries)
66+ -- avoid adding empty strings
77+ if text ~= "" then
88+ table.insert(yanks, 1, text)
99+1010+ if #yanks > max_entries then
1111+ table.remove(yanks)
1212+ end
1313+ end
1414+end
1515+1616+-- autocommand to listen for yank events
1717+function M.setup_yank_autocmd(yanks, max_entries)
1818+ vim.api.nvim_create_autocmd("TextYankPost", {
1919+ callback = function()
2020+ -- TODO: this function can be expanded to incorporate other registers
2121+ -- - use vim.v.event.regname and an allowlist
2222+ -- if reg_type == "y" or reg_type == "d" then
2323+ -- local yanked_text = vim.fn.getreg('"')
2424+2525+ -- get register information
2626+ local reg_type = vim.v.event.operator
2727+ local rn = vim.v.event.regname
2828+2929+ -- check changes wwere made to default register
3030+ if vim.v.event.regname == '' then
3131+ local yanked_text = vim.fn.getreg(rn)
3232+3333+ -- don't track single character deletions
3434+ if #yanked_text <= 1 and reg_type ~= "y" then
3535+ return
3636+ end
3737+3838+ M.add_yank(yanks, yanked_text, max_entries)
3939+ end
4040+ end,
4141+ })
4242+end
4343+4444+return M
+53
lua/yankbank/data.lua
···11+-- data.lua
22+local M = {}
33+44+-- reformat yanks table for popup
55+function M.get_display_lines(yanks)
66+ local display_lines = {}
77+ local line_yank_map = {}
88+ local yank_num = 0
99+1010+ -- calculate the maximum width needed for the yank numbers
1111+ local max_digits = #tostring(#yanks)
1212+1313+ for i, yank in ipairs(yanks) do
1414+ yank_num = yank_num + 1
1515+1616+ -- remove trailing newlines
1717+ yank = yank:gsub("\n$", "")
1818+ local yank_lines = vim.split(yank, "\n", { plain= true })
1919+ local leading_space, leading_space_length
2020+2121+ -- determine the number of leading whitespaces on the first line
2222+ if #yank_lines > 0 then
2323+ leading_space = yank_lines[1]:match("^(%s*)")
2424+ leading_space_length = #leading_space
2525+ end
2626+2727+ for j, line in ipairs(yank_lines) do
2828+ if j == 1 then
2929+ -- Format the line number with uniform spacing
3030+ local lineNumber = string.format("%" .. max_digits .. "d: ", yank_num)
3131+ line = line:sub(leading_space_length + 1)
3232+ table.insert(display_lines, lineNumber .. line)
3333+ else
3434+ -- Remove the same amount of leading whitespace as on the first line
3535+ line = line:sub(leading_space_length + 1)
3636+ -- Use spaces equal to the line number's reserved space to align subsequent lines
3737+ table.insert(display_lines, string.rep(" ", max_digits + 2) .. line)
3838+ end
3939+ table.insert(line_yank_map, i)
4040+ end
4141+4242+ -- Add a visual separator between yanks, aligned with the yank content
4343+ -- TODO: allow turning off/on in plugin setup
4444+ if i < #yanks then
4545+ table.insert(display_lines, string.rep(" ", max_digits + 2) .. "------")
4646+ table.insert(line_yank_map, false)
4747+ end
4848+ end
4949+5050+ return display_lines, line_yank_map
5151+end
5252+5353+return M
+45
lua/yankbank/helpers.lua
···11+-- helpers.lua
22+local M = {}
33+44+-- navigate to the next numbered item
55+function M.next_numbered_item()
66+ local current_line = vim.api.nvim_win_get_cursor(0)[1]
77+ local total_lines = vim.api.nvim_buf_line_count(0)
88+ for i = current_line + 1, total_lines do
99+ local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1]
1010+ -- search for the correct line start
1111+ if line:match("^%s*%d+:") then
1212+ vim.api.nvim_win_set_cursor(0, {i, 0})
1313+ break
1414+ end
1515+ end
1616+end
1717+1818+1919+-- navigate to the previous numbered item
2020+function M.prev_numbered_item()
2121+ local current_line = vim.api.nvim_win_get_cursor(0)[1]
2222+ for i = current_line - 1, 1, -1 do
2323+ local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1]
2424+ -- search for the correct line start
2525+ if line:match("^%s*%d+:") then
2626+ vim.api.nvim_win_set_cursor(0, {i, 0})
2727+ break
2828+ end
2929+ end
3030+end
3131+3232+-- customized paste function that functions more like 'p'
3333+function M.smart_paste(text)
3434+ -- determine if the text should be treated as line-wise based on its ending
3535+ if text:sub(-1) == '\n' then
3636+ -- line-wise
3737+ vim.cmd("normal! o")
3838+ vim.api.nvim_paste(text, false, -1)
3939+ else
4040+ -- character-wise
4141+ vim.api.nvim_paste(text, false, -1)
4242+ end
4343+end
4444+4545+return M
+40
lua/yankbank/init.lua
···11+-- init.lua
22+local M = {}
33+44+-- local imports
55+local menu = require("yankbank.menu")
66+local clipboard = require("yankbank.clipboard")
77+88+-- initialize yanks tables
99+local yanks = {}
1010+local max_entries = 10
1111+1212+-- wrapper function for main plugin functionality
1313+local function show_yank_bank()
1414+ -- TODO: update max entries with passed in options
1515+ local bufnr, display_lines, line_yank_map = menu.create_and_fill_buffer(yanks, max_entries)
1616+ local win_id = menu.open_window(bufnr, display_lines)
1717+ menu.set_keymaps(win_id, bufnr, yanks, line_yank_map)
1818+end
1919+2020+-- plugin setup
2121+function M.setup(opts)
2222+ local o = {}
2323+2424+ -- parse opts
2525+ if opts ~= nil then
2626+ o.max_entries = opts.max_entries or max_entries
2727+ else
2828+ o.max_entries = max_entries
2929+ end
3030+3131+ -- create clipboard autocmds
3232+ clipboard.setup_yank_autocmd(yanks, o.max_entries)
3333+3434+ -- Create user command
3535+ -- TODO: allow params (i.e. keymaps/max_entries/separator)
3636+ vim.api.nvim_create_user_command("YankBank", show_yank_bank,
3737+ { desc = "Show Recent Yanks" })
3838+end
3939+4040+return M
+111
lua/yankbank/menu.lua
···11+-- menu.lua
22+local M = {}
33+44+-- import clipboard functions
55+local clipboard = require("yankbank.clipboard")
66+local data = require("yankbank.data")
77+local helpers = require("yankbank.helpers")
88+99+-- create new buffer and reformat yank table for ui
1010+function M.create_and_fill_buffer(yanks, max_entries)
1111+ -- check the content of the system clipboard register
1212+ -- TODO: this could be replaced with some sort of polling of the + register
1313+ local text = vim.fn.getreg('+')
1414+ local most_recent_yank = yanks[1] or ""
1515+ if text ~= most_recent_yank then
1616+ clipboard.add_yank(yanks, text, max_entries)
1717+ end
1818+1919+ -- stop if yank table is empty
2020+ if #yanks == 0 then
2121+ print("No yanks to show.")
2222+ return
2323+ end
2424+2525+ -- create new buffer
2626+ local bufnr = vim.api.nvim_create_buf(false, true)
2727+2828+ -- set buffer type same as current window for syntax highlighting
2929+ local current_filetype = vim.bo.filetype
3030+ vim.api.nvim_buf_set_option(bufnr, 'filetype', current_filetype)
3131+3232+ local display_lines, line_yank_map = data.get_display_lines(yanks)
3333+3434+ -- replace current buffer contents with updated table
3535+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, display_lines)
3636+3737+ return bufnr, display_lines, line_yank_map
3838+end
3939+4040+-- Calculate size and create popup window from bufnr
4141+function M.open_window(bufnr, display_lines)
4242+ -- set maximum window width based on number of lines
4343+ local max_width = 0
4444+ for _, line in ipairs(display_lines) do
4545+ max_width = math.max(max_width, #line)
4646+ end
4747+4848+ -- define buffer window width and height based on number of columns
4949+ local width = math.min(max_width + 4, vim.api.nvim_get_option("columns") - 50)
5050+ local height = math.min(#display_lines, vim.api.nvim_get_option("lines") - 4)
5151+5252+ -- open window
5353+ local win_id = vim.api.nvim_open_win(bufnr, true, {
5454+ relative = "editor",
5555+ width = width,
5656+ height = height,
5757+ col = math.floor((vim.api.nvim_get_option("columns") - width) / 2 - 1),
5858+ row = math.floor((vim.api.nvim_get_option("lines") - height) / 2) - 1,
5959+ border = "rounded",
6060+ style = "minimal",
6161+ })
6262+6363+ -- Highlight current line
6464+ vim.api.nvim_win_set_option(win_id, 'cursorline', true)
6565+6666+ return win_id
6767+end
6868+6969+-- Set key mappings for the popup window
7070+-- TODO: configurable options (take in inside setup function)
7171+function M.set_keymaps(win_id, bufnr, yanks, line_yank_map)
7272+ -- Key mappings for selection and closing the popup
7373+ local map_opts = { noremap = true, silent = true, buffer = bufnr }
7474+7575+ -- popup buffer navigation binds
7676+ vim.keymap.set('n', 'j', helpers.next_numbered_item,
7777+ { noremap = true, silent = true, buffer = bufnr })
7878+ vim.keymap.set('n', 'k', helpers.prev_numbered_item,
7979+ { noremap = true, silent = true, buffer = bufnr })
8080+8181+ -- bind paste behavior to enter
8282+ vim.keymap.set('n', '<CR>', function()
8383+ local cursor = vim.api.nvim_win_get_cursor(win_id)[1]
8484+ -- use the mapping to find the original yank
8585+ local yankIndex = line_yank_map[cursor]
8686+ if yankIndex then
8787+ -- retrieve the full yank, including all lines
8888+ local text = yanks[yankIndex]
8989+9090+ -- close window upon selection
9191+ vim.api.nvim_win_close(win_id, true)
9292+9393+ -- call the custom paste function with adjusted indentation
9494+ print(vim.fn.getregtype())
9595+ -- vim.api.nvim_paste(text, false, -1)
9696+ helpers.smart_paste(text)
9797+ else
9898+ print("Error: Invalid selection")
9999+ end
100100+ end, { buffer = bufnr })
101101+102102+ -- close popup keybinds
103103+ local close_maps = { "<Esc>", "<C-c>", "q" }
104104+ for _, map in ipairs(close_maps) do
105105+ vim.keymap.set('n', map, function()
106106+ vim.api.nvim_win_close(win_id, true)
107107+ end, map_opts)
108108+ end
109109+end
110110+111111+return M