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

Configure Feed

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

feat: working sql persistence

ptdewey 8f1283dd e0f1451a

+76 -372
-1
lua/yankbank/clipboard.lua
··· 1 - -- clipboard.lua 2 1 local M = {} 3 2 4 3 -- import persistence module
+3 -11
lua/yankbank/data.lua
··· 1 - -- data.lua 2 1 local M = {} 3 2 4 3 ---reformat yanks table for popup ··· 36 35 for j, line in ipairs(yank_lines) do 37 36 if j == 1 then 38 37 -- Format the line number with uniform spacing 39 - local lineNumber = 40 - string.format("%" .. max_digits .. "d: ", yank_num) 38 + local lineNumber = string.format("%" .. max_digits .. "d: ", yank_num) 41 39 line = line:sub(leading_space_length + 1) 42 40 table.insert(display_lines, lineNumber .. line) 43 41 else 44 42 -- Remove the same amount of leading whitespace as on the first line 45 43 line = line:sub(leading_space_length + 1) 46 44 -- Use spaces equal to the line number's reserved space to align subsequent lines 47 - table.insert( 48 - display_lines, 49 - string.rep(" ", max_digits + 2) .. line 50 - ) 45 + table.insert(display_lines, string.rep(" ", max_digits + 2) .. line) 51 46 end 52 47 table.insert(line_yank_map, i) 53 48 end ··· 55 50 if i < #yanks then 56 51 -- Add a visual separator between yanks, aligned with the yank content 57 52 if sep ~= "" then 58 - table.insert( 59 - display_lines, 60 - string.rep(" ", max_digits + 2) .. sep 61 - ) 53 + table.insert(display_lines, string.rep(" ", max_digits + 2) .. sep) 62 54 end 63 55 table.insert(line_yank_map, false) 64 56 end
-1
lua/yankbank/helpers.lua
··· 1 - -- helpers.lua 2 1 local M = {} 3 2 4 3 -- navigate to the next numbered item
+5 -8
lua/yankbank/init.lua
··· 1 - -- init.lua 2 1 local M = {} 3 2 4 3 -- local imports ··· 23 22 persist_path = plugin_path .. "bank.txt", 24 23 } 25 24 26 - -- wrapper function for main plugin functionality 25 + --- wrapper function for main plugin functionality 27 26 ---@param opts table 27 + --- TODO: read from persistent database if sql persist is set (allow multi-session sync) 28 28 local function show_yank_bank(opts) 29 29 -- create and fill buffer 30 - local bufnr, display_lines, line_yank_map = 31 - menu.create_and_fill_buffer(yanks, reg_types, opts) 30 + local bufnr, display_lines, line_yank_map = menu.create_and_fill_buffer(yanks, reg_types, opts) 32 31 33 32 -- handle empty bank case 34 33 if not bufnr or not display_lines or not line_yank_map then ··· 45 44 -- merge opts with default options table 46 45 opts = vim.tbl_deep_extend("force", default_opts, opts or {}) 47 46 48 - -- enable persistence based on opts 49 - -- (needs to be called before autocmd setup) 50 - persistence.setup(yanks, reg_types, opts) 51 - -- yanks, reg_types = persistence.setup(yanks, reg_types, opts) 47 + -- enable persistence based on opts (needs to be called before autocmd setup) 48 + yanks, reg_types = persistence.setup(opts) 52 49 53 50 -- create clipboard autocmds 54 51 clipboard.setup_yank_autocmd(yanks, reg_types, opts)
+4 -11
lua/yankbank/menu.lua
··· 1 - -- menu.lua 2 1 local M = {} 3 2 4 3 -- import clipboard functions ··· 61 60 -- define buffer window width and height based on number of columns 62 61 -- FIX: long enough entries will cause window to go below end of screen 63 62 -- FIX: wrapping long lines will cause entries below to not show in menu (requires scrolling to see) 64 - local width = 65 - math.min(max_width, vim.api.nvim_get_option_value("columns", {}) - 4) 63 + local width = math.min(max_width, vim.api.nvim_get_option_value("columns", {}) - 4) 66 64 local height = math.min( 67 65 display_lines and #display_lines or 1, 68 66 vim.api.nvim_get_option_value("lines", {}) - 10 ··· 73 71 relative = "editor", 74 72 width = width, 75 73 height = height, 76 - col = math.floor( 77 - (vim.api.nvim_get_option_value("columns", {}) - width) / 2 78 - ), 79 - row = math.floor( 80 - (vim.api.nvim_get_option_value("lines", {}) - height) / 2 81 - ), 74 + col = math.floor((vim.api.nvim_get_option_value("columns", {}) - width) / 2), 75 + row = math.floor((vim.api.nvim_get_option_value("lines", {}) - height) / 2), 82 76 border = "rounded", 83 77 style = "minimal", 84 78 }) ··· 118 112 local k = vim.tbl_deep_extend("force", default_keymaps, opts.keymaps or {}) 119 113 120 114 -- merge default and options keymap tables 121 - opts.registers = 122 - vim.tbl_deep_extend("force", default_registers, opts.registers or {}) 115 + opts.registers = vim.tbl_deep_extend("force", default_registers, opts.registers or {}) 123 116 124 117 -- check table for number behavior option (prefix or jump, default to prefix) 125 118 opts.num_behavior = opts.num_behavior or "prefix"
+6 -21
lua/yankbank/persistence.lua
··· 1 - -- persistence.lua 2 1 local M = {} 3 2 4 3 local persistence = {} 5 - local db = nil 6 4 7 5 ---add entry from bank to 8 6 ---@param entry string|table ··· 11 9 function M.add_entry(entry, reg_type, opts) 12 10 if not opts.persist_type then 13 11 return 14 - elseif opts.persist_type == "file" then 15 - persistence.add_to_bankfile(opts.persist_path, entry, reg_type) 16 12 elseif opts.persist_type == "sqlite" then 17 - persistence.add_to_yanktable(db, entry, reg_type) 13 + persistence:insert_yank(entry, reg_type) 18 14 end 19 15 end 20 16 21 17 ---initialize bank persistence 22 - ---@param yanks table 23 - ---@param reg_types table 24 18 ---@param opts table 25 19 ---@return table 26 20 ---@return table 27 - function M.setup(yanks, reg_types, opts) 21 + function M.setup(opts) 28 22 if not opts.persist_type then 29 23 return {}, {} 30 - elseif opts.persist_type == "file" then 31 - persistence = require("yankbank.persistence.file") 32 - return persistence.setup_persistence( 33 - opts.persist_path, 34 - opts.max_entries, 35 - yanks, 36 - reg_types 37 - ) 38 24 elseif opts.persist_type == "sqlite" then 39 - persistence = require("yankbank.persistence.sql") 40 - db = persistence.init_db(yanks, reg_types, opts.persist_path) 41 - return yanks, reg_types 25 + persistence = require("yankbank.persistence.sql").setup(opts) 26 + return persistence:get_bank() 27 + else 28 + return {}, {} 42 29 end 43 - 44 - return {}, {} 45 30 end 46 31 47 32 return M
-238
lua/yankbank/persistence/file.lua
··· 1 - -- persistence/file.lua 2 - local M = {} 3 - 4 - local n_entries = 0 5 - local m_entries = 10 6 - 7 - ---function that checks if a file exists 8 - ---@param file string: file path 9 - ---@return boolean 10 - local function file_exists(file) 11 - local f = io.open(file, "rb") 12 - if f then 13 - f:close() 14 - end 15 - return f ~= nil 16 - end 17 - 18 - ---function that reads all lines of file into a table 19 - ---@param file string: file path 20 - ---@return table 21 - local function read_lines(file) 22 - local f, err = io.open(file) 23 - if not f then 24 - error("Error opening file: " .. err) 25 - end 26 - local lines = {} 27 - for line in f:lines() do 28 - lines[#lines + 1] = line 29 - end 30 - f:close() 31 - return lines 32 - end 33 - 34 - ---check first line from file for presence of yankbank list header. 35 - ---if it exists, populate current number of entries. 36 - ---@param line string 37 - ---@return boolean 38 - local function check_for_header(line) 39 - local n = string.match(line, "<YANKBANK_LIST:(%d+)>") 40 - if n then 41 - n_entries = tonumber(n, 10) 42 - return true 43 - end 44 - return false 45 - end 46 - 47 - ---function that checks for the presence of a yankbank header on a given line. 48 - ---returns t/f and index, length for entries that exist 49 - ---@param line string: line from file being checked 50 - ---@return table|nil 51 - local function check_for_entry(line) 52 - local i, l, rt = string.match(line, "<YANKBANK_ENTRY:(%d+),(%d+),(%a+)>") 53 - if i then 54 - return { 55 - index = tonumber(i), 56 - length = tonumber(l), 57 - reg_type = rt, 58 - } 59 - end 60 - end 61 - 62 - ---get line count of a string 63 - ---@param str string 64 - ---@return integer 65 - local function get_line_count(str) 66 - local lines = 1 67 - for i = 1, #str do 68 - local c = str:sub(i, i) 69 - if c == "\n" then 70 - lines = lines + 1 71 - end 72 - end 73 - return lines 74 - end 75 - 76 - ---function that reads a yankbank entry from an index to an offset. 77 - ---@param i integer: starting index 78 - ---@param offset integer: stopping point = i+offset 79 - ---@param lines table: file contents 80 - ---@return table: yankbank entry in string table form 81 - local function read_entry(i, offset, lines) 82 - local entry = {} 83 - for j = i, i + offset - 1 do 84 - entry[#entry + 1] = lines[j] 85 - end 86 - -- handle extra newline added to end of entry in bank file 87 - if #entry > 1 then 88 - table.remove(entry) 89 - end 90 - return entry 91 - end 92 - 93 - ---remove entry from bankfile 94 - ---@param file string: bank file name 95 - local function remove_last_entry(file) 96 - local f, err = io.open(file, "r+") 97 - if not f then 98 - error("Could not open file for reading: " .. err) 99 - end 100 - 101 - -- read lines from file until matching entry is found 102 - local lines = {} 103 - for line in f:lines() do 104 - if 105 - string.match(line, "<YANKBANK_ENTRY:" .. n_entries .. ",%d+,%a+>") 106 - then 107 - n_entries = n_entries - 1 108 - lines[1] = "<YANKBANK_LIST:" .. n_entries .. ">" 109 - break 110 - else 111 - lines[#lines + 1] = line 112 - end 113 - end 114 - f:close() 115 - 116 - -- write to file 117 - f, err = io.open(file, "w") 118 - if not f then 119 - error("Could not open file for writing: " .. err) 120 - end 121 - for i = 1, #lines do 122 - f:write(lines[i] .. "\n") 123 - end 124 - f:close() 125 - end 126 - 127 - ---add entry bankfile. (this function needs to be callable from outside the module) 128 - ---@param file string 129 - ---@param entry table|string 130 - ---@param reg_type string 131 - function M.add_to_bankfile(file, entry, reg_type) 132 - -- remove last entry if new capacity would exceed maximum 133 - if n_entries >= m_entries then 134 - remove_last_entry(file) 135 - end 136 - n_entries = n_entries + 1 137 - 138 - local lines = read_lines(file) 139 - local f, err = io.open(file, "w+") 140 - if not f then 141 - error("Could not open file: " .. err) 142 - end 143 - 144 - -- add list header 145 - f:write("<YANKBANK_LIST:" .. n_entries .. ">\n") 146 - 147 - -- get line count of entry (special case for strings) 148 - local len = #entry 149 - if type(entry) == "string" then 150 - len = get_line_count(entry) 151 - end 152 - 153 - -- write entry header 154 - f:write("<YANKBANK_ENTRY:1," .. len .. "," .. reg_type .. ">\n") 155 - -- write new entry 156 - if type(entry) == "string" then 157 - f:write(entry .. "\n") 158 - else 159 - for i = 1, #entry do 160 - f:write(entry[i] .. "\n") 161 - end 162 - end 163 - 164 - -- write back previous entries 165 - for i = 2, #lines do 166 - local n, l, rt = 167 - string.match(lines[i], "<YANKBANK_ENTRY:(%d+),(%d+),(%a+)>") 168 - if n then 169 - lines[i] = "<YANKBANK_ENTRY:" 170 - .. n + 1 171 - .. "," 172 - .. l 173 - .. "," 174 - .. rt 175 - .. ">" 176 - end 177 - f:write(lines[i] .. "\n") 178 - end 179 - 180 - f:close() 181 - end 182 - 183 - ---populate yankbank with entries contained in file. 184 - ---@param yanks table: table to populate with yanks 185 - ---@param file string: yankbank persistence file 186 - ---@param max_entries integer: maximum number of yankbank entries 187 - ---@return table, table 188 - local function populate_yankbank(file, max_entries, yanks, reg_types) 189 - -- read lines from file 190 - local lines = read_lines(file) 191 - if not check_for_header(lines[1]) then 192 - print("YankBank list header not found in file...") 193 - return {}, {} 194 - end 195 - 196 - -- iterate through remaining lines in file, adding entries to yankbank 197 - local i = 2 198 - while i <= #lines do 199 - local res = check_for_entry(lines[i]) 200 - if res then 201 - local entry = read_entry(i + 1, res.length, lines) 202 - if res.index < max_entries then 203 - yanks[#yanks + 1] = entry 204 - reg_types[#reg_types + 1] = res.reg_type 205 - end 206 - -- skip lines that were added to entries 207 - i = i + res.length 208 - end 209 - i = i + 1 210 - end 211 - return yanks, reg_types 212 - end 213 - 214 - ---setup function for a persistence file. 215 - ---should be called in plugin setup function 216 - ---@param file string: file path 217 - ---@param max_entries integer: maximum number of yankbank entries 218 - ---@param yanks table: table to populate with yanks 219 - ---@param reg_types table: table containing register types 220 - ---@return table, table 221 - function M.setup_persistence(file, max_entries, yanks, reg_types) 222 - -- check if file exists, otherwise create it (with header) 223 - if not file_exists(file) then 224 - print("Creating file...") 225 - local f, err = io.open(file, "w") 226 - if f then 227 - f:write("<YANKBANK_LIST:0>") 228 - f:close() 229 - else 230 - print(err) 231 - end 232 - return {}, {} 233 - end 234 - m_entries = max_entries 235 - return populate_yankbank(file, max_entries, yanks, reg_types) 236 - end 237 - 238 - return M
+58 -81
lua/yankbank/persistence/sql.lua
··· 2 2 3 3 local sqlite = require("sqlite.db") 4 4 5 - -- TODO: yank primary key? 6 - -- integer tracking for table position not controlled by sqlite3 5 + local dbdir = vim.fn.stdpath("data") .. "/databases" 6 + local max_entries = 10 7 7 8 - ---create db table for yanks, PK is row id and will increment automatically 9 - -- @param existing_yanks table 10 - -- @param reg_types table 11 - -- @param uri string 12 - -- @return sqlite_db 13 - function M.init_db(existing_yanks, reg_types, uri) 14 - local db = sqlite({ 15 - uri = uri, 16 - }) 17 - db:open() 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 18 21 19 - db:create("yanks", { 20 - id = true, 21 - yank_content = { "text", required = true }, 22 - reg_type = { "text", required = true }, 23 - ensure = true, 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, 24 35 }) 25 - local status = db:status() 26 36 27 - db:insert("yanks", { yank_content = existing_yanks, reg_type = reg_types }) 37 + -- attempt to trim database size 38 + self:trim_size() 39 + end 28 40 29 - if status ~= nil then 30 - print("yankbank db error: ", status.code) 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 }) 31 46 end 32 - -- TODO: add functionality to add existing yanks to the db table "yanks" 33 - db:close() 34 - return db 35 47 end 36 48 37 - -- add entry to DB 38 - -- @param db sqlite_db 39 - -- @param yank_content string 40 - -- @param reg_type string 41 - -- @return boolean 42 - function M.add_to_yanktable(db, yank_content, reg_type) 43 - db:open() 44 - db:insert("yanks", { yank_content = yank_content, reg_type = reg_type }) 45 - local status = db:status() 46 - db:close() 47 - return status == nil 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 48 61 end 49 62 50 - -- removes entry from yanktable 51 - -- @param db sqlite_db 52 - -- @param yank_content string 53 - -- @return boolean 54 - function M.remove_from_yanktable(db, yank_content) 55 - db:open() 56 - db:delete("yanks", { where = { yank_content = yank_content } }) 57 - local status = db:status() 58 - db:close() 59 - return status == nil 60 - end 63 + -- FIX: correctly handle multiple sessions open at once 64 + -- - fetch database state each time YankBank command is called? 61 65 62 - -- returns all yanks in table sorted by recency descending 63 - -- @param db sqlite_db 64 - -- @return table[] 65 - function M.get_yanks(db) 66 - db:open() 67 - local ret = db:select("yanks", { order_by = { asc = "id" } }) 68 - db:close() 69 - return ret 70 - end 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 71 72 - function M.remove_by_yank_index(db, index) 73 - db:open() 74 - local ret = db:select("yanks", { order_by = { asc = "id" } }) 75 - local id_to_remove = ret[index].id 76 - local del = db:delete("yanks", { where = { id = id_to_remove } }) 77 - if del ~= nil then 78 - return del 72 + if vim.fn.isdirectory(dbdir) == 0 then 73 + vim.fn.mkdir(dbdir, "p") 79 74 end 80 - local status = db:status() 81 - db:close() 82 - return status == nil 83 - end 84 75 85 - -- test function for db operations 86 - local function test_database() 87 - local test_db = M.init_db({}, {}, "/tmp/test_yankbank.db") 88 - -- print(vim.inspect(test_db)) 89 - M.add_to_yanktable(test_db, "Sample Yank", "reg") 90 - print(vim.inspect(M.get_yanks(test_db))) 91 - M.add_to_yanktable(test_db, "Sample Different Yank", "reg") 92 - M.remove_from_yanktable(test_db, "Sample Different Yank") 93 - print("after SDY delete") 94 - print(vim.inspect(M.get_yanks(test_db))) 95 - M.remove_by_yank_index(test_db, 2) 96 - print("after index 2 delete") 97 - print(vim.inspect(M.get_yanks(test_db))) 76 + return data 98 77 end 99 - 100 - test_database() 101 78 102 79 return M