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 #19 from ptdewey/feat-lua-api

fix/feat: Fixed edge case insert behavior and API Improvements

authored by

Patrick Dewey and committed by
GitHub
2fb46c72 a568cb99

+197 -37
+1 -1
.github/workflows/docs.yaml
··· 20 20 - name: Push changes 21 21 uses: stefanzweifel/git-auto-commit-action@v4 22 22 with: 23 - commit_message: "auto-generate vimdoc" 23 + commit_message: "doc: auto-generate vimdoc" 24 24 commit_user_name: "github-actions[bot]" 25 25 commit_user_email: "github-actions[bot]@users.noreply.github.com" 26 26 commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
+1 -1
Makefile
··· 4 4 5 5 lint: 6 6 echo "Linting lua/yankbank..." 7 - luacheck lua/ --globals vim YB_YANKS YB_REG_TYPES YB_OPTS 7 + luacheck lua/ --globals vim YB_YANKS YB_REG_TYPES YB_OPTS YB_PINS 8 8 9 9 pr-ready: fmt lint
+43 -5
lua/yankbank/api.lua
··· 26 26 --- add an entry to yankbank 27 27 ---@param yank_text string yank text to add to YANKS table 28 28 ---@param reg_type string register type "v", "V", or "^V" (visual, v-line, v-block respectively) 29 - function M.add_entry(yank_text, reg_type) 30 - require("yankbank.clipboard").add_yank(yank_text, reg_type) 29 + ---@param pin integer|boolean? 30 + function M.add_entry(yank_text, reg_type, pin) 31 + require("yankbank.clipboard").add_yank(yank_text, reg_type, pin) 31 32 end 32 33 33 34 --- remove entry from yankbank by index 34 35 ---@param i integer index to remove 35 36 function M.remove_entry(i) 36 37 local yank_text = table.remove(YB_YANKS, i) 37 - table.remove(YB_REG_TYPES, i) 38 + local reg_type = table.remove(YB_REG_TYPES, i) 38 39 if YB_OPTS.persist_type == "sqlite" then 39 - require("yankbank.persistence.sql").data().remove_match(yank_text) 40 + require("yankbank.persistence.sql") 41 + .data() 42 + .remove_match(yank_text, reg_type) 43 + end 44 + end 45 + 46 + --- pin entry to yankbank so that it won't be removed when its position exceeds the max number of entries 47 + --- 48 + ---@param i integer index to pin 49 + function M.pin_entry(i) 50 + if i > #YB_PINS then 51 + return 52 + end 53 + 54 + -- TODO: show pins differently in popup (could use different hl_groups for pinned entries?) 55 + YB_PINS[i] = 1 56 + 57 + if YB_OPTS.persist_type == "sqlite" then 58 + return require("yankbank.persistence.sql") 59 + .data() 60 + .pin(YB_YANKS[i], YB_REG_TYPES[i]) 61 + end 62 + end 63 + 64 + --- unpin bank entry 65 + --- 66 + ---@param i integer index to unpin 67 + function M.unpin_entry(i) 68 + if i > #YB_PINS then 69 + return 70 + end 71 + 72 + -- TODO: update popup pin highlight 73 + YB_PINS[i] = 0 74 + 75 + if YB_OPTS.persist_type == "sqlite" then 76 + return require("yankbank.persistence.sql") 77 + .data() 78 + .unpin(YB_YANKS[i], YB_REG_TYPES[i]) 40 79 end 41 80 end 42 81 43 82 -- TODO: individual popup keymap setting functions 44 - -- - could just update opts table that is passed into set_keymaps 45 83 46 84 return M
+26 -4
lua/yankbank/clipboard.lua
··· 2 2 3 3 -- import persistence module 4 4 local persistence = require("yankbank.persistence") 5 + local utils = require("yankbank.utils") 5 6 6 7 --- Function to add yanked text to table 7 8 ---@param text string 8 9 ---@param reg_type string 9 - function M.add_yank(text, reg_type) 10 + ---@param pin integer|boolean? 11 + function M.add_yank(text, reg_type, pin) 10 12 -- avoid adding empty strings 11 13 if text == "" and text == " " and text == "\n" then 12 14 return 13 15 end 14 16 17 + local is_pinned = 0 18 + 15 19 -- check for duplicate values already inserted 16 20 for i, entry in ipairs(YB_YANKS) do 17 21 if entry == text then 18 22 -- remove matched entry so it can be inserted at 1st position 19 23 table.remove(YB_YANKS, i) 20 24 table.remove(YB_REG_TYPES, i) 25 + is_pinned = table.remove(YB_PINS, i) 21 26 break 22 27 end 23 28 end 24 29 30 + -- override is_pinned if pin is set 31 + is_pinned = (pin == 1 or pin == true) and 1 32 + or (pin == 0 or pin == false) and 0 33 + or is_pinned 34 + 25 35 -- add entry to bank 26 36 table.insert(YB_YANKS, 1, text) 27 37 table.insert(YB_REG_TYPES, 1, reg_type) 38 + table.insert(YB_PINS, 1, is_pinned) 28 39 29 40 -- trim table size if necessary 30 41 if #YB_YANKS > YB_OPTS.max_entries then 31 - table.remove(YB_YANKS) 32 - table.remove(YB_REG_TYPES) 42 + local i = utils.last_zero_entry(YB_PINS) 43 + 44 + if not i or i == 1 then 45 + -- WARN: undefined behavior 46 + print( 47 + "Warning: all YankBank entries are pinned, insertion behavior is undefined when all entries are pinned." 48 + ) 49 + else 50 + -- remove last non-pinned entry 51 + table.remove(YB_YANKS, i) 52 + table.remove(YB_REG_TYPES, i) 53 + table.remove(YB_PINS, i) 54 + end 33 55 end 34 56 35 57 -- add entry to persistent store 36 - persistence.add_entry(text, reg_type) 58 + persistence.add_entry(text, reg_type, pin) 37 59 end 38 60 39 61 --- autocommand to listen for yank events
+2 -1
lua/yankbank/init.lua
··· 3 3 -- define global variables 4 4 YB_YANKS = {} 5 5 YB_REG_TYPES = {} 6 + YB_PINS = {} 6 7 YB_OPTS = {} 7 8 8 9 -- local imports ··· 47 48 YB_OPTS = vim.tbl_deep_extend("keep", opts or {}, default_opts) 48 49 49 50 -- enable persistence based on opts (needs to be called before autocmd setup) 50 - YB_YANKS, YB_REG_TYPES = persistence.setup() 51 + YB_YANKS, YB_REG_TYPES, YB_PINS = persistence.setup() 51 52 52 53 -- create clipboard autocmds 53 54 clipboard.setup_yank_autocmd()
+6 -4
lua/yankbank/persistence.lua
··· 5 5 ---add entry from bank to 6 6 ---@param entry string 7 7 ---@param reg_type string 8 - function M.add_entry(entry, reg_type) 8 + ---@param pin integer|boolean? 9 + function M.add_entry(entry, reg_type, pin) 9 10 if YB_OPTS.persist_type == "sqlite" then 10 - persistence:insert_yank(entry, reg_type) 11 + persistence:insert_yank(entry, reg_type, pin) 11 12 end 12 13 end 13 14 ··· 21 22 ---initialize bank persistence 22 23 ---@return table 23 24 ---@return table 25 + ---@return table 24 26 function M.setup() 25 27 if not YB_OPTS.persist_type then 26 - return {}, {} 28 + return {}, {}, {} 27 29 elseif YB_OPTS.persist_type == "sqlite" then 28 30 persistence = require("yankbank.persistence.sql").setup() 29 31 return persistence:get_bank() 30 32 else 31 - return {}, {} 33 + return {}, {}, {} 32 34 end 33 35 end 34 36
+102 -21
lua/yankbank/persistence/sql.lua
··· 1 1 local M = {} 2 2 3 - local sqlite = require("sqlite.db") 3 + local sqlite = require("sqlite") 4 4 5 5 -- local dbdir = vim.fn.stdpath("data") .. "/databases" 6 6 local dbdir = debug.getinfo(1).source:sub(2):match("(.*/).*/.*/.*/") or "./" ··· 8 8 9 9 ---@class YankBankDB:sqlite_db 10 10 ---@field bank sqlite_tbl 11 + ---@field bank sqlite_tbl 11 12 local db = sqlite({ 12 13 uri = dbdir .. "/yankbank.db", 13 14 bank = { 14 15 -- yanked text should be unique and be primary key 15 16 yank_text = { "text", unique = true, primary = true, required = true }, 16 17 reg_type = { "text", required = true }, 18 + pinned = { "integer", required = true, default = 0 }, 17 19 }, 18 20 }) 19 21 20 22 ---@class sqlite_tbl 21 23 local data = db.bank 22 24 25 + -- NOTE: escape and unescape query text 26 + -- TODO: adjust to only escape text that matches function syntax 27 + --- 28 + ---@param content string 29 + ---@return string 30 + function M.escape(content) 31 + return string.format("__ESCAPED__'%s'", content) 32 + end 33 + 34 + --- 35 + ---@param content string 36 + ---@return string 37 + ---@return integer? 38 + function M.unescape(content) 39 + return content:gsub("^__ESCAPED__'(.*)'$", "%1") 40 + end 41 + 23 42 --- insert yank entry into database 24 43 ---@param yank_text string yanked text 25 44 ---@param reg_type string register type 26 - function data:insert_yank(yank_text, reg_type) 45 + ---@param pin integer|boolean? pin status of inserted entry 46 + function data:insert_yank(yank_text, reg_type, pin) 27 47 -- attempt to remove entry if count > 0 (to move potential duplicate) 48 + local is_pinned = 0 28 49 if self:count() > 0 then 29 50 db:with_open(function() 51 + -- check if entry exists in db 52 + local res = db:eval( 53 + "SELECT * FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 54 + { yank_text = M.escape(yank_text), reg_type = reg_type } 55 + ) 56 + 57 + -- if result is empty (eval returns boolean), proceed to insertion 58 + if type(res) == "boolean" then 59 + return 60 + end 61 + 62 + -- entry found, get pin status 63 + is_pinned = res[1].pinned 64 + 65 + -- remove entry from db so it can be moved to first position 30 66 db:eval( 31 - "DELETE FROM bank WHERE yank_text = :yank_text", 32 - { yank_text = yank_text } 67 + "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 68 + { yank_text = M.escape(yank_text), reg_type = reg_type } 33 69 ) 34 70 end) 35 71 end 36 72 73 + -- override is_pinned if pin param is set, default to is_pinned otherwise 74 + is_pinned = (pin == 1 or pin == true) and 1 75 + or (pin == 0 or pin == false) and 0 76 + or is_pinned 77 + 37 78 -- insert entry using the eval method with parameterized query to avoid error on 'data:insert()' 38 79 db:with_open(function() 39 80 db:eval( 40 - "INSERT INTO bank (yank_text, reg_type) VALUES (:yank_text, :reg_type)", 41 - { yank_text = yank_text, reg_type = reg_type } 81 + "INSERT INTO bank (yank_text, reg_type, pinned) VALUES (:yank_text, :reg_type, :pinned)", 82 + { 83 + yank_text = M.escape(yank_text), 84 + reg_type = reg_type, 85 + pinned = is_pinned, 86 + } 42 87 ) 43 88 end) 44 89 ··· 47 92 end 48 93 49 94 --- trim database size if it exceeds max_entries option 95 + --- WARN: if all entries are pinned, behavior is undefined 50 96 function data:trim_size() 51 97 if self:count() > max_entries then 52 98 -- remove the oldest entry 53 - local oldest_entry = db:with_open(function() 54 - return db:select( 55 - "bank", 56 - { order_by = { asc = "rowid" }, limit = { 1 } } 57 - )[1] 99 + local e = db:with_open(function() 100 + return db:select("bank", { 101 + where = { pinned = 0 }, 102 + order_by = { asc = "rowid" }, 103 + limit = 1, 104 + })[1] 58 105 end) 59 106 60 - if oldest_entry then 107 + if e then 61 108 db:with_open(function() 62 109 db:eval( 63 110 "DELETE FROM bank WHERE yank_text = :yank_text", 64 - { yank_text = oldest_entry.yank_text } 111 + { yank_text = e.yank_text } 65 112 ) 66 113 end) 67 114 end ··· 69 116 end 70 117 71 118 --- get sqlite bank contents 72 - ---@return table yanks, table reg_types 119 + ---@return table yanks, table reg_types, table pins 73 120 function data:get_bank() 74 - local yanks, reg_types = {}, {} 121 + local yanks, reg_types, pins = {}, {}, {} 75 122 76 123 local bank = self:get() 77 124 for _, entry in ipairs(bank) do 78 - table.insert(yanks, 1, entry.yank_text) 125 + local text, _ = M.unescape(entry.yank_text) 126 + table.insert(yanks, 1, text) 79 127 table.insert(reg_types, 1, entry.reg_type) 128 + table.insert(pins, 1, entry.pinned) 80 129 end 81 130 82 - return yanks, reg_types 131 + return yanks, reg_types, pins 83 132 end 84 133 85 134 --- remove an entry from the banks table matching input text 86 135 ---@param text string 87 - function data.remove_match(text) 136 + ---@param reg_type string 137 + function data.remove_match(text, reg_type) 88 138 db:with_open(function() 89 139 return db:eval( 90 - "DELETE FROM bank WHERE yank_text = :yank_text", 91 - { yank_text = text } 140 + "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 141 + { yank_text = M.escape(text), reg_type = reg_type } 142 + ) 143 + end) 144 + end 145 + 146 + --- pin entry in yankbank to prevent removal 147 + ---@param text string text to match and pin 148 + ---@param reg_type string reg_type corresponding to text 149 + ---@return boolean 150 + function data.pin(text, reg_type) 151 + return db:with_open(function() 152 + -- TODO: always returns true or nothing 153 + return ( 154 + db:eval( 155 + "UPDATE bank SET pinned = 1 WHERE yank_text = :yank_text and reg_type = :reg_type", 156 + { yank_text = M.escape(text), reg_type = reg_type } 157 + ) 158 + ) 159 + end) 160 + end 161 + 162 + --- unpin entry in yankbank to prevent removal 163 + ---@param text string 164 + ---@param reg_type string reg_type corresponding to text 165 + ---@return boolean 166 + function data.unpin(text, reg_type) 167 + return db:with_open(function() 168 + -- TODO: always returns true or nothing 169 + -- - figure out how to return if updated or remove return 170 + return db:eval( 171 + "UPDATE bank SET pinned = 0 WHERE yank_text = :yank_text and reg_type = :reg_type", 172 + { yank_text = M.escape(text), reg_type = reg_type } 92 173 ) 93 174 end) 94 175 end ··· 105 186 max_entries = YB_OPTS.max_entries 106 187 107 188 vim.api.nvim_create_user_command("YankBankClearDB", function() 108 - data:remove() 189 + data:drop() 109 190 YB_YANKS = {} 110 191 YB_REG_TYPES = {} 111 192 end, {})
+16
lua/yankbank/utils.lua
··· 1 + local M = {} 2 + 3 + --- get the last zero entry in a table 4 + --- 5 + ---@param t table 6 + ---@return integer? 7 + function M.last_zero_entry(t) 8 + for i = #t, 1, -1 do 9 + if t[i] == 0 then 10 + return i 11 + end 12 + end 13 + return nil 14 + end 15 + 16 + return M