Neovim plugin improving access to clipboard history (mirror)
at main 203 lines 5.9 kB view raw
1local M = {} 2 3local sqlite = require("sqlite") 4local state = require("yankbank.state") 5 6local function get_db_path() 7 local opts = state.get_opts() 8 return opts.db_path 9 or debug.getinfo(1).source:sub(2):match("(.*/).*/.*/.*/") 10 or "./" 11end 12 13local max_entries = 10 14 15---@class YankBankDB:sqlite_db 16---@field bank sqlite_tbl 17---@field bank sqlite_tbl 18local db = sqlite({ 19 uri = get_db_path() .. "/yankbank.db", 20 bank = { 21 -- yanked text should be unique and be primary key 22 yank_text = { "text", unique = true, primary = true, required = true }, 23 reg_type = { "text", required = true }, 24 pinned = { "integer", required = true, default = 0 }, 25 }, 26}) 27 28---@class sqlite_tbl 29local data = db.bank 30 31---@param content string 32---@return string 33function M.escape(content) 34 return string.format("__ESCAPED__'%s'", content) 35end 36 37---@param content string 38---@return string 39---@return integer? 40function M.unescape(content) 41 return content:gsub("^__ESCAPED__'(.*)'$", "%1") 42end 43 44--- insert yank entry into database 45---@param yank_text string yanked text 46---@param reg_type string register type 47---@param pin integer|boolean? pin status of inserted entry 48function data:insert_yank(yank_text, reg_type, pin) 49 -- attempt to remove entry if count > 0 (to move potential duplicate) 50 local is_pinned = 0 51 if self:count() > 0 then 52 db:with_open(function() 53 -- check if entry exists in db 54 local res = db:eval( 55 "SELECT * FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 56 { yank_text = M.escape(yank_text), reg_type = reg_type } 57 ) 58 59 -- if result is empty (eval returns boolean), proceed to insertion 60 if type(res) == "boolean" then 61 return 62 end 63 64 -- entry found, get pin status 65 is_pinned = res[1].pinned 66 67 -- remove entry from db so it can be moved to first position 68 db:eval( 69 "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 70 { yank_text = M.escape(yank_text), reg_type = reg_type } 71 ) 72 end) 73 end 74 75 -- override is_pinned if pin param is set, default to is_pinned otherwise 76 is_pinned = (pin == 1 or pin == true) and 1 77 or (pin == 0 or pin == false) and 0 78 or is_pinned 79 80 -- insert entry using the eval method with parameterized query to avoid error on 'data:insert()' 81 db:with_open(function() 82 db:eval( 83 "INSERT INTO bank (yank_text, reg_type, pinned) VALUES (:yank_text, :reg_type, :pinned)", 84 { 85 yank_text = M.escape(yank_text), 86 reg_type = reg_type, 87 pinned = is_pinned, 88 } 89 ) 90 end) 91 92 -- attempt to trim database size 93 self:trim_size() 94end 95 96--- trim database size if it exceeds max_entries option 97--- WARN: if all entries are pinned, behavior is undefined 98function data:trim_size() 99 if self:count() > max_entries then 100 -- remove the oldest entry 101 local e = db:with_open(function() 102 return db:select("bank", { 103 where = { pinned = 0 }, 104 order_by = { asc = "rowid" }, 105 limit = 1, 106 })[1] 107 end) 108 109 if e then 110 db:with_open(function() 111 db:eval( 112 "DELETE FROM bank WHERE yank_text = :yank_text", 113 { yank_text = e.yank_text } 114 ) 115 end) 116 end 117 end 118end 119 120--- get sqlite bank contents 121---@return table yanks, table reg_types, table pins 122function data:get_bank() 123 local yanks, reg_types, pins = {}, {}, {} 124 125 local bank = self:get() 126 for _, entry in ipairs(bank) do 127 local text, _ = M.unescape(entry.yank_text) 128 table.insert(yanks, 1, text) 129 table.insert(reg_types, 1, entry.reg_type) 130 table.insert(pins, 1, entry.pinned) 131 end 132 133 return yanks, reg_types, pins 134end 135 136--- remove an entry from the banks table matching input text 137---@param text string 138---@param reg_type string 139function data.remove_match(text, reg_type) 140 db:with_open(function() 141 return db:eval( 142 "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type", 143 { yank_text = M.escape(text), reg_type = reg_type } 144 ) 145 end) 146end 147 148--- pin entry in yankbank to prevent removal 149---@param text string text to match and pin 150---@param reg_type string reg_type corresponding to text 151---@return boolean? 152function data.pin(text, reg_type) 153 return db:with_open(function() 154 return ( 155 db:eval( 156 "UPDATE bank SET pinned = 1 WHERE yank_text = :yank_text and reg_type = :reg_type", 157 { yank_text = M.escape(text), reg_type = reg_type } 158 ) 159 ) 160 end) 161end 162 163--- unpin entry in yankbank to prevent removal 164---@param text string 165---@param reg_type string reg_type corresponding to text 166---@return boolean? 167function data.unpin(text, reg_type) 168 return db:with_open(function() 169 return db:eval( 170 "UPDATE bank SET pinned = 0 WHERE yank_text = :yank_text and reg_type = :reg_type", 171 { yank_text = M.escape(text), reg_type = reg_type } 172 ) 173 end) 174end 175 176--- get data in sqlite_tbl form (for api use only) 177---@return sqlite_tbl 178function M.data() 179 return data 180end 181 182--- set up database persistence 183---@return sqlite_tbl data 184function M.setup() 185 local opts = state.get_opts() 186 max_entries = opts.max_entries 187 188 vim.api.nvim_create_user_command("YankBankClearDB", function() 189 data:drop() 190 state.set_yanks({}) 191 state.set_reg_types({}) 192 end, {}) 193 194 if opts.debug == true then 195 vim.api.nvim_create_user_command("YankBankViewDB", function() 196 print(vim.inspect(data:get())) 197 end, {}) 198 end 199 200 return data 201end 202 203return M