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

Configure Feed

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

Merge pull request #7 from ptdewey/feature-4-persistence

feat(bank persistence)

authored by

Patrick Dewey and committed by
GitHub
2998a904 3a4e48d2

+236 -62
+24 -3
README.md
··· 31 31 Packer: 32 32 ```lua 33 33 use { 34 - 'ptdewey/yankbank-nvim', 34 + "ptdewey/yankbank-nvim", 35 35 config = function() 36 36 require('yankbank').setup() 37 37 end, ··· 55 55 | focus_gain_poll | boolean | `nil` | 56 56 | registers | table container for register overrides | `{ }` | 57 57 | registers.yank_register | default register to yank from popup to | `"+"` | 58 + | persist_type | string defining persistence type "memory" or "sqlite" | `"memory"` | 59 + | persist_path | string defining path for persistence file/db file | `"~/.local/share/nvim/lazy/yankbank-nvim"` (if installed with lazy) | 58 60 59 61 60 62 #### Example Configuration ··· 70 72 navigation_next = "j", 71 73 navigation_prev = "k", 72 74 }, 75 + num_behavior = "prefix", 76 + persist_type = "sqlite", 73 77 registers = { 74 78 yank_register = "+", 75 79 }, ··· 84 88 - `num_behavior = "jump"` jumps to entry matching the pressed number key (i.e. '3' jumps to entry 3) 85 89 - Note: If 'max_entries' is a two-digit number, there will be a delay upon pressing numbers that prefix a valid entry. 86 90 91 + #### Persistence 92 + If persistence between sessions is desired, sqlite.lua will be used to create a persistent store for recent yanks in the plugin root directory. 93 + To utilize sqlite persistence, `"kkharji/sqlite.lua"` must be added as a dependency in your config, and `persist_type` must be set to `"sqlite"`: 94 + 95 + ```lua 96 + -- lazy 97 + return { 98 + "ptdewey/yankbank-nvim", 99 + dependencies = "kkharji/sqlite.lua", 100 + config = function() 101 + require('yankbank').setup({ 102 + -- other options... 103 + persist_type = "sqlite" 104 + }) 105 + end, 106 + } 107 + ``` 87 108 88 109 The 'focus_gain_poll' option allows for enabling an additional autocommand that watches for focus gains (refocusing Neovim window), and checks for changes in the unnamedplus ('+') register, adding to yankbank when new contents are found. This allows for automatically adding text copied from other sources (like a browser) to the yankbank without the bank opening trigger. Off by default, but I highly recommend enabling it (`focus_gain_poll = true`) 89 110 ··· 100 121 101 122 102 123 ## Potential Improvements 103 - 104 - - Persistence between sessions (through either sqlite database or just a file) 124 + - Polling on unnamedplus register to populate bank in more intuitive manner (could be enabled as option) 105 125 - nvim-cmp integration 106 126 - fzf integration 127 + - telescope integration 107 128 108 129 ## Alternatives 109 130
+23 -23
lua/yankbank/clipboard.lua
··· 1 - -- clipboard.lua 2 1 local M = {} 3 2 3 + -- import persistence module 4 + local persistence = require("yankbank.persistence") 5 + 4 6 -- Function to add yanked text to table 5 - function M.add_yank(yanks, reg_types, text, reg_type, max_entries) 7 + ---@param yanks table 8 + ---@param reg_types table 9 + ---@param text string 10 + ---@param reg_type string 11 + ---@param opts table 12 + function M.add_yank(yanks, reg_types, text, reg_type, opts) 6 13 -- avoid adding empty strings 7 - if text == "" and text == " " and text == "\n" then 14 + -- TODO: could block adding single characters here 15 + if text == "" or text == " " or text == "\n" then 8 16 return 9 17 end 10 18 ··· 15 23 end 16 24 end 17 25 26 + -- add entry to bank 18 27 table.insert(yanks, 1, text) 19 28 table.insert(reg_types, 1, reg_type) 20 - 21 - if #yanks > max_entries then 29 + if #yanks > opts.max_entries then 22 30 table.remove(yanks) 23 31 table.remove(reg_types) 24 32 end 33 + 34 + -- add entry to persistent store 35 + persistence.add_entry(text, reg_type, opts) 25 36 end 26 37 27 - -- set up plugin autocommands 28 - -- TODO: make augroup 38 + -- autocommand to listen for yank events 39 + ---@param yanks table 40 + ---@param reg_types table 41 + ---@param opts table 29 42 function M.setup_yank_autocmd(yanks, reg_types, opts) 30 - -- autocommand to listen for yank events 31 43 vim.api.nvim_create_autocmd("TextYankPost", { 32 44 callback = function() 33 45 -- get register information 34 46 local rn = vim.v.event.regname 35 47 36 - -- check if changes were made to default register 48 + -- check changes wwere made to default register 37 49 if rn == "" or rn == "+" then 38 50 local reg_type = vim.fn.getregtype(rn) 39 51 local yank_text = vim.fn.getreg(rn) ··· 45 57 if #yank_text <= 1 then 46 58 return 47 59 end 48 - M.add_yank( 49 - yanks, 50 - reg_types, 51 - yank_text, 52 - reg_type, 53 - opts.max_entries 54 - ) 60 + M.add_yank(yanks, reg_types, yank_text, reg_type, opts) 55 61 end 56 62 end, 57 63 }) ··· 72 78 return 73 79 end 74 80 75 - M.add_yank( 76 - yanks, 77 - reg_types, 78 - yank_text, 79 - reg_type, 80 - opts.max_entries 81 - ) 81 + M.add_yank(yanks, reg_types, yank_text, reg_type, opts) 82 82 end, 83 83 }) 84 84 end
+13 -5
lua/yankbank/data.lua
··· 1 - -- data.lua 2 1 local M = {} 3 2 4 - -- reformat yanks table for popup 3 + ---reformat yanks table for popup 4 + ---@param yanks table 5 + ---@param sep string 6 + ---@return table, table 5 7 function M.get_display_lines(yanks, sep) 6 8 local display_lines = {} 7 9 local line_yank_map = {} ··· 10 12 -- calculate the maximum width needed for the yank numbers 11 13 local max_digits = #tostring(#yanks) 12 14 15 + -- assumes yanks is table of strings 13 16 for i, yank in ipairs(yanks) do 14 17 yank_num = yank_num + 1 15 18 16 - -- remove trailing newlines 17 - yank = yank:gsub("\n$", "") 18 - local yank_lines = vim.split(yank, "\n", { plain = true }) 19 + -- FIX: there were changes here, might need further changes 20 + local yank_lines = yank 21 + if type(yank) == "string" then 22 + -- remove trailing newlines 23 + yank = yank:gsub("\n$", "") 24 + yank_lines = vim.split(yank, "\n", { plain = true }) 25 + end 26 + 19 27 local leading_space, leading_space_length 20 28 21 29 -- determine the number of leading whitespaces on the first line
+18 -7
lua/yankbank/helpers.lua
··· 1 - -- helpers.lua 2 1 local M = {} 3 2 4 3 -- navigate to the next numbered item 4 + ---@param steps integer 5 5 function M.next_numbered_item(steps) 6 6 steps = steps or 1 -- Default to 1 if no steps are provided 7 7 local current_line = vim.api.nvim_win_get_cursor(0)[1] ··· 24 24 end 25 25 26 26 -- navigate to the previous numbered item 27 + ---@param steps integer 27 28 function M.prev_numbered_item(steps) 28 29 steps = steps or 1 -- Default to 1 if no steps are provided 29 30 local current_line = vim.api.nvim_win_get_cursor(0)[1] ··· 43 44 end 44 45 45 46 -- customized paste function that functions like 'p' 47 + ---@param text string|table 48 + ---@param reg_type string 46 49 function M.smart_paste(text, reg_type) 47 - -- convert text string to string list 48 50 local lines = {} 49 - for line in text:gmatch("([^\n]*)\n?") do 50 - table.insert(lines, line) 51 + if type(text) == "string" then 52 + -- convert text string to string list 53 + for line in text:gmatch("([^\n]*)\n?") do 54 + table.insert(lines, line) 55 + end 56 + if #lines > 1 then 57 + table.remove(lines) 58 + end 59 + else 60 + -- text is already table 61 + lines = text 51 62 end 52 63 53 64 -- remove last newline character to replicate base put behavior 54 - if #lines > 1 then 55 - table.remove(lines) 56 - end 65 + -- if lines[#lines] == "" then 66 + -- table.remove(lines) 67 + -- end 57 68 vim.api.nvim_put(lines, reg_type, true, true) 58 69 end 59 70
+18 -9
lua/yankbank/init.lua
··· 1 - -- init.lua 2 1 local M = {} 3 2 4 3 -- local imports 5 4 local menu = require("yankbank.menu") 6 5 local clipboard = require("yankbank.clipboard") 6 + local persistence = require("yankbank.persistence") 7 7 8 8 -- initialize yanks tables 9 9 local yanks = {} 10 10 local reg_types = {} 11 + 12 + -- default plugin options 11 13 local default_opts = { 12 14 max_entries = 10, 13 15 sep = "-----", ··· 16 18 registers = { 17 19 yank_register = "+", 18 20 }, 21 + persist_type = "memory", 19 22 keymaps = {}, 20 23 } 21 24 22 - -- wrapper function for main plugin functionality 25 + -- local plugin_path = debug.getinfo(1).source:sub(2):match("(.*/).*/.*/") or "./" 26 + 27 + --- wrapper function for main plugin functionality 28 + ---@param opts table 29 + --- TODO: read from persistent database if sql persist is set (allow multi-session sync) 23 30 local function show_yank_bank(opts) 24 31 -- Parse command arguments directly if args are provided as a string 25 32 opts = opts or default_opts 26 33 27 34 -- initialize buffer and populate bank 28 - local bufnr, display_lines, line_yank_map = menu.create_and_fill_buffer( 29 - yanks, 30 - reg_types, 31 - opts.max_entries, 32 - opts.sep 33 - ) 35 + local bufnr, display_lines, line_yank_map = 36 + menu.create_and_fill_buffer(yanks, reg_types, opts) 34 37 35 38 -- handle empty bank case 36 - if not bufnr then 39 + if not bufnr or not display_lines or not line_yank_map then 37 40 return 38 41 end 39 42 ··· 43 46 end 44 47 45 48 -- plugin setup 49 + ---@param opts table 46 50 function M.setup(opts) 51 + -- merge opts with default options table 52 + -- opts = vim.tbl_deep_extend("force", default_opts, opts or {}) 47 53 opts = opts or default_opts 54 + 55 + -- enable persistence based on opts (needs to be called before autocmd setup) 56 + yanks, reg_types = persistence.setup(opts) 48 57 49 58 -- create clipboard autocmds 50 59 clipboard.setup_yank_autocmd(yanks, reg_types, opts)
+28 -15
lua/yankbank/menu.lua
··· 1 - -- menu.lua 2 1 local M = {} 3 2 4 3 -- import clipboard functions 5 - local clipboard = require("yankbank.clipboard") 4 + -- local clipboard = require("yankbank.clipboard") 6 5 local data = require("yankbank.data") 7 6 local helpers = require("yankbank.helpers") 8 7 9 - -- create new buffer and reformat yank table for ui 10 - function M.create_and_fill_buffer(yanks, reg_types, max_entries, sep) 8 + ---create new buffer and reformat yank table for ui 9 + ---@param yanks table 10 + ---@param reg_types table 11 + ---@param opts table 12 + ---@return integer? 13 + ---@return table? 14 + ---@return table? 15 + function M.create_and_fill_buffer(yanks, reg_types, opts) 11 16 -- check the content of the system clipboard register 12 17 -- 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 - local reg_type = vim.fn.getregtype("+") 17 - clipboard.add_yank(yanks, reg_types, text, reg_type, max_entries) 18 - end 18 + -- local text = vim.fn.getreg("+") 19 + -- local most_recent_yank = yanks[1] or "" 20 + -- local reg_type = vim.fn.getregtype("+") 21 + -- clipboard.add_yank(yanks, reg_types, text, reg_type, opts) 19 22 20 23 -- stop if yank table is empty 21 - if #yanks == 0 then 24 + if #yanks == 0 and #reg_types then 22 25 print("No yanks to show.") 23 - return 26 + return nil, nil, nil 24 27 end 25 28 26 29 -- create new buffer ··· 30 33 local current_filetype = vim.bo.filetype 31 34 vim.api.nvim_set_option_value("filetype", current_filetype, { buf = bufnr }) 32 35 33 - local display_lines, line_yank_map = data.get_display_lines(yanks, sep) 36 + -- TODO: need to update yanks from bank file before get_display_lines 37 + local display_lines, line_yank_map = data.get_display_lines(yanks, opts.sep) 34 38 35 39 -- replace current buffer contents with updated table 36 40 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, display_lines) ··· 38 42 return bufnr, display_lines, line_yank_map 39 43 end 40 44 41 - -- Calculate size and create popup window from bufnr 45 + ---Calculate size and create popup window from bufnr 46 + ---@param bufnr integer 47 + ---@param display_lines table 48 + ---@return integer 42 49 function M.open_window(bufnr, display_lines) 43 50 -- set maximum window width based on number of lines 44 51 local max_width = 0 ··· 81 88 return win_id 82 89 end 83 90 84 - -- Set key mappings for the popup window 91 + ---Set key mappings for the popup window 92 + ---@param win_id integer 93 + ---@param bufnr integer 94 + ---@param yanks table 95 + ---@param reg_types table 96 + ---@param line_yank_map table 97 + ---@param opts table 85 98 function M.set_keymaps(win_id, bufnr, yanks, reg_types, line_yank_map, opts) 86 99 -- Key mappings for selection and closing the popup 87 100 local map_opts = { noremap = true, silent = true, buffer = bufnr }
+32
lua/yankbank/persistence.lua
··· 1 + local M = {} 2 + 3 + local persistence = {} 4 + 5 + ---add entry from bank to 6 + ---@param entry string|table 7 + ---@param reg_type string 8 + ---@param opts table 9 + function M.add_entry(entry, reg_type, opts) 10 + if not opts.persist_type then 11 + return 12 + elseif opts.persist_type == "sqlite" then 13 + persistence:insert_yank(entry, reg_type) 14 + end 15 + end 16 + 17 + ---initialize bank persistence 18 + ---@param opts table 19 + ---@return table 20 + ---@return table 21 + function M.setup(opts) 22 + if not opts.persist_type then 23 + return {}, {} 24 + elseif opts.persist_type == "sqlite" then 25 + persistence = require("yankbank.persistence.sql").setup(opts) 26 + return persistence:get_bank() 27 + else 28 + return {}, {} 29 + end 30 + end 31 + 32 + return M
+80
lua/yankbank/persistence/sql.lua
··· 1 + local M = {} 2 + 3 + local sqlite = require("sqlite.db") 4 + 5 + local dbdir = vim.fn.stdpath("data") .. "/databases" 6 + local max_entries = 10 7 + 8 + ---@class YankBankDB:sqlite_db 9 + ---@field bank sqlite_tbl 10 + local db = sqlite({ 11 + uri = dbdir .. "/yankbank.db", 12 + bank = { 13 + -- yanked text should be unique and be primary key 14 + yank_text = { "text", unique = true, primary = true, required = true }, 15 + reg_type = { "text", required = true }, 16 + }, 17 + }) 18 + 19 + ---@class sqlite_tbl 20 + local data = db.bank 21 + 22 + --- insert yank entry into database 23 + ---@param yank_text string yanked text 24 + ---@param reg_type string register type 25 + function data:insert_yank(yank_text, reg_type) 26 + -- attempt to remove entry if count > 0 (to move potential duplicate) 27 + if self:count() > 0 then 28 + self:remove({ yank_text = yank_text }) 29 + end 30 + 31 + -- insert entry 32 + self:insert({ 33 + yank_text = yank_text, 34 + reg_type = reg_type, 35 + }) 36 + 37 + -- attempt to trim database size 38 + self:trim_size() 39 + end 40 + 41 + --- trim database size if it exceeds max_entries option 42 + function data:trim_size() 43 + if self:count() > max_entries then 44 + -- remove the oldest entry 45 + self:remove({ yank_text = self:get()[1].yank_text }) 46 + end 47 + end 48 + 49 + --- get sqlite bank contents 50 + ---@return table yanks, table reg_types 51 + function data:get_bank() 52 + local yanks, reg_types = {}, {} 53 + 54 + local bank = self:get() 55 + for _, entry in ipairs(bank) do 56 + table.insert(yanks, 1, entry.yank_text) 57 + table.insert(reg_types, 1, entry.reg_type) 58 + end 59 + 60 + return yanks, reg_types 61 + end 62 + 63 + -- FIX: correctly handle multiple sessions open at once 64 + -- - fetch database state each time YankBank command is called? 65 + 66 + --- set up database persistence 67 + ---@param opts table 68 + ---@return sqlite_tbl data 69 + function M.setup(opts) 70 + max_entries = opts.max_entries 71 + 72 + -- TODO: move database into plugin directory instead to allow easier uninstall 73 + if vim.fn.isdirectory(dbdir) == 0 then 74 + vim.fn.mkdir(dbdir, "p") 75 + end 76 + 77 + return data 78 + end 79 + 80 + return M