···2626--- add an entry to yankbank
2727---@param yank_text string yank text to add to YANKS table
2828---@param reg_type string register type "v", "V", or "^V" (visual, v-line, v-block respectively)
2929-function M.add_entry(yank_text, reg_type)
3030- require("yankbank.clipboard").add_yank(yank_text, reg_type)
2929+---@param pin integer|boolean?
3030+function M.add_entry(yank_text, reg_type, pin)
3131+ require("yankbank.clipboard").add_yank(yank_text, reg_type, pin)
3132end
32333334--- remove entry from yankbank by index
3435---@param i integer index to remove
3536function M.remove_entry(i)
3637 local yank_text = table.remove(YB_YANKS, i)
3737- table.remove(YB_REG_TYPES, i)
3838+ local reg_type = table.remove(YB_REG_TYPES, i)
3839 if YB_OPTS.persist_type == "sqlite" then
3939- require("yankbank.persistence.sql").data().remove_match(yank_text)
4040+ require("yankbank.persistence.sql")
4141+ .data()
4242+ .remove_match(yank_text, reg_type)
4343+ end
4444+end
4545+4646+--- pin entry to yankbank so that it won't be removed when its position exceeds the max number of entries
4747+---
4848+---@param i integer index to pin
4949+function M.pin_entry(i)
5050+ if i > #YB_PINS then
5151+ return
5252+ end
5353+5454+ -- TODO: show pins differently in popup (could use different hl_groups for pinned entries?)
5555+ YB_PINS[i] = 1
5656+5757+ if YB_OPTS.persist_type == "sqlite" then
5858+ return require("yankbank.persistence.sql")
5959+ .data()
6060+ .pin(YB_YANKS[i], YB_REG_TYPES[i])
6161+ end
6262+end
6363+6464+--- unpin bank entry
6565+---
6666+---@param i integer index to unpin
6767+function M.unpin_entry(i)
6868+ if i > #YB_PINS then
6969+ return
7070+ end
7171+7272+ -- TODO: update popup pin highlight
7373+ YB_PINS[i] = 0
7474+7575+ if YB_OPTS.persist_type == "sqlite" then
7676+ return require("yankbank.persistence.sql")
7777+ .data()
7878+ .unpin(YB_YANKS[i], YB_REG_TYPES[i])
4079 end
4180end
42814382-- TODO: individual popup keymap setting functions
4444--- - could just update opts table that is passed into set_keymaps
45834684return M
+26-4
lua/yankbank/clipboard.lua
···2233-- import persistence module
44local persistence = require("yankbank.persistence")
55+local utils = require("yankbank.utils")
5667--- Function to add yanked text to table
78---@param text string
89---@param reg_type string
99-function M.add_yank(text, reg_type)
1010+---@param pin integer|boolean?
1111+function M.add_yank(text, reg_type, pin)
1012 -- avoid adding empty strings
1113 if text == "" and text == " " and text == "\n" then
1214 return
1315 end
14161717+ local is_pinned = 0
1818+1519 -- check for duplicate values already inserted
1620 for i, entry in ipairs(YB_YANKS) do
1721 if entry == text then
1822 -- remove matched entry so it can be inserted at 1st position
1923 table.remove(YB_YANKS, i)
2024 table.remove(YB_REG_TYPES, i)
2525+ is_pinned = table.remove(YB_PINS, i)
2126 break
2227 end
2328 end
24293030+ -- override is_pinned if pin is set
3131+ is_pinned = (pin == 1 or pin == true) and 1
3232+ or (pin == 0 or pin == false) and 0
3333+ or is_pinned
3434+2535 -- add entry to bank
2636 table.insert(YB_YANKS, 1, text)
2737 table.insert(YB_REG_TYPES, 1, reg_type)
3838+ table.insert(YB_PINS, 1, is_pinned)
28392940 -- trim table size if necessary
3041 if #YB_YANKS > YB_OPTS.max_entries then
3131- table.remove(YB_YANKS)
3232- table.remove(YB_REG_TYPES)
4242+ local i = utils.last_zero_entry(YB_PINS)
4343+4444+ if not i or i == 1 then
4545+ -- WARN: undefined behavior
4646+ print(
4747+ "Warning: all YankBank entries are pinned, insertion behavior is undefined when all entries are pinned."
4848+ )
4949+ else
5050+ -- remove last non-pinned entry
5151+ table.remove(YB_YANKS, i)
5252+ table.remove(YB_REG_TYPES, i)
5353+ table.remove(YB_PINS, i)
5454+ end
3355 end
34563557 -- add entry to persistent store
3636- persistence.add_entry(text, reg_type)
5858+ persistence.add_entry(text, reg_type, pin)
3759end
38603961--- autocommand to listen for yank events
+2-1
lua/yankbank/init.lua
···33-- define global variables
44YB_YANKS = {}
55YB_REG_TYPES = {}
66+YB_PINS = {}
67YB_OPTS = {}
7889-- local imports
···4748 YB_OPTS = vim.tbl_deep_extend("keep", opts or {}, default_opts)
48494950 -- enable persistence based on opts (needs to be called before autocmd setup)
5050- YB_YANKS, YB_REG_TYPES = persistence.setup()
5151+ YB_YANKS, YB_REG_TYPES, YB_PINS = persistence.setup()
51525253 -- create clipboard autocmds
5354 clipboard.setup_yank_autocmd()
+6-4
lua/yankbank/persistence.lua
···55---add entry from bank to
66---@param entry string
77---@param reg_type string
88-function M.add_entry(entry, reg_type)
88+---@param pin integer|boolean?
99+function M.add_entry(entry, reg_type, pin)
910 if YB_OPTS.persist_type == "sqlite" then
1010- persistence:insert_yank(entry, reg_type)
1111+ persistence:insert_yank(entry, reg_type, pin)
1112 end
1213end
1314···2122---initialize bank persistence
2223---@return table
2324---@return table
2525+---@return table
2426function M.setup()
2527 if not YB_OPTS.persist_type then
2626- return {}, {}
2828+ return {}, {}, {}
2729 elseif YB_OPTS.persist_type == "sqlite" then
2830 persistence = require("yankbank.persistence.sql").setup()
2931 return persistence:get_bank()
3032 else
3131- return {}, {}
3333+ return {}, {}, {}
3234 end
3335end
3436
+102-21
lua/yankbank/persistence/sql.lua
···11local M = {}
2233-local sqlite = require("sqlite.db")
33+local sqlite = require("sqlite")
4455-- local dbdir = vim.fn.stdpath("data") .. "/databases"
66local dbdir = debug.getinfo(1).source:sub(2):match("(.*/).*/.*/.*/") or "./"
···8899---@class YankBankDB:sqlite_db
1010---@field bank sqlite_tbl
1111+---@field bank sqlite_tbl
1112local db = sqlite({
1213 uri = dbdir .. "/yankbank.db",
1314 bank = {
1415 -- yanked text should be unique and be primary key
1516 yank_text = { "text", unique = true, primary = true, required = true },
1617 reg_type = { "text", required = true },
1818+ pinned = { "integer", required = true, default = 0 },
1719 },
1820})
19212022---@class sqlite_tbl
2123local data = db.bank
22242525+-- NOTE: escape and unescape query text
2626+-- TODO: adjust to only escape text that matches function syntax
2727+---
2828+---@param content string
2929+---@return string
3030+function M.escape(content)
3131+ return string.format("__ESCAPED__'%s'", content)
3232+end
3333+3434+---
3535+---@param content string
3636+---@return string
3737+---@return integer?
3838+function M.unescape(content)
3939+ return content:gsub("^__ESCAPED__'(.*)'$", "%1")
4040+end
4141+2342--- insert yank entry into database
2443---@param yank_text string yanked text
2544---@param reg_type string register type
2626-function data:insert_yank(yank_text, reg_type)
4545+---@param pin integer|boolean? pin status of inserted entry
4646+function data:insert_yank(yank_text, reg_type, pin)
2747 -- attempt to remove entry if count > 0 (to move potential duplicate)
4848+ local is_pinned = 0
2849 if self:count() > 0 then
2950 db:with_open(function()
5151+ -- check if entry exists in db
5252+ local res = db:eval(
5353+ "SELECT * FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
5454+ { yank_text = M.escape(yank_text), reg_type = reg_type }
5555+ )
5656+5757+ -- if result is empty (eval returns boolean), proceed to insertion
5858+ if type(res) == "boolean" then
5959+ return
6060+ end
6161+6262+ -- entry found, get pin status
6363+ is_pinned = res[1].pinned
6464+6565+ -- remove entry from db so it can be moved to first position
3066 db:eval(
3131- "DELETE FROM bank WHERE yank_text = :yank_text",
3232- { yank_text = yank_text }
6767+ "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
6868+ { yank_text = M.escape(yank_text), reg_type = reg_type }
3369 )
3470 end)
3571 end
36727373+ -- override is_pinned if pin param is set, default to is_pinned otherwise
7474+ is_pinned = (pin == 1 or pin == true) and 1
7575+ or (pin == 0 or pin == false) and 0
7676+ or is_pinned
7777+3778 -- insert entry using the eval method with parameterized query to avoid error on 'data:insert()'
3879 db:with_open(function()
3980 db:eval(
4040- "INSERT INTO bank (yank_text, reg_type) VALUES (:yank_text, :reg_type)",
4141- { yank_text = yank_text, reg_type = reg_type }
8181+ "INSERT INTO bank (yank_text, reg_type, pinned) VALUES (:yank_text, :reg_type, :pinned)",
8282+ {
8383+ yank_text = M.escape(yank_text),
8484+ reg_type = reg_type,
8585+ pinned = is_pinned,
8686+ }
4287 )
4388 end)
4489···4792end
48934994--- trim database size if it exceeds max_entries option
9595+--- WARN: if all entries are pinned, behavior is undefined
5096function data:trim_size()
5197 if self:count() > max_entries then
5298 -- remove the oldest entry
5353- local oldest_entry = db:with_open(function()
5454- return db:select(
5555- "bank",
5656- { order_by = { asc = "rowid" }, limit = { 1 } }
5757- )[1]
9999+ local e = db:with_open(function()
100100+ return db:select("bank", {
101101+ where = { pinned = 0 },
102102+ order_by = { asc = "rowid" },
103103+ limit = 1,
104104+ })[1]
58105 end)
591066060- if oldest_entry then
107107+ if e then
61108 db:with_open(function()
62109 db:eval(
63110 "DELETE FROM bank WHERE yank_text = :yank_text",
6464- { yank_text = oldest_entry.yank_text }
111111+ { yank_text = e.yank_text }
65112 )
66113 end)
67114 end
···69116end
7011771118--- get sqlite bank contents
7272----@return table yanks, table reg_types
119119+---@return table yanks, table reg_types, table pins
73120function data:get_bank()
7474- local yanks, reg_types = {}, {}
121121+ local yanks, reg_types, pins = {}, {}, {}
7512276123 local bank = self:get()
77124 for _, entry in ipairs(bank) do
7878- table.insert(yanks, 1, entry.yank_text)
125125+ local text, _ = M.unescape(entry.yank_text)
126126+ table.insert(yanks, 1, text)
79127 table.insert(reg_types, 1, entry.reg_type)
128128+ table.insert(pins, 1, entry.pinned)
80129 end
811308282- return yanks, reg_types
131131+ return yanks, reg_types, pins
83132end
8413385134--- remove an entry from the banks table matching input text
86135---@param text string
8787-function data.remove_match(text)
136136+---@param reg_type string
137137+function data.remove_match(text, reg_type)
88138 db:with_open(function()
89139 return db:eval(
9090- "DELETE FROM bank WHERE yank_text = :yank_text",
9191- { yank_text = text }
140140+ "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
141141+ { yank_text = M.escape(text), reg_type = reg_type }
142142+ )
143143+ end)
144144+end
145145+146146+--- pin entry in yankbank to prevent removal
147147+---@param text string text to match and pin
148148+---@param reg_type string reg_type corresponding to text
149149+---@return boolean
150150+function data.pin(text, reg_type)
151151+ return db:with_open(function()
152152+ -- TODO: always returns true or nothing
153153+ return (
154154+ db:eval(
155155+ "UPDATE bank SET pinned = 1 WHERE yank_text = :yank_text and reg_type = :reg_type",
156156+ { yank_text = M.escape(text), reg_type = reg_type }
157157+ )
158158+ )
159159+ end)
160160+end
161161+162162+--- unpin entry in yankbank to prevent removal
163163+---@param text string
164164+---@param reg_type string reg_type corresponding to text
165165+---@return boolean
166166+function data.unpin(text, reg_type)
167167+ return db:with_open(function()
168168+ -- TODO: always returns true or nothing
169169+ -- - figure out how to return if updated or remove return
170170+ return db:eval(
171171+ "UPDATE bank SET pinned = 0 WHERE yank_text = :yank_text and reg_type = :reg_type",
172172+ { yank_text = M.escape(text), reg_type = reg_type }
92173 )
93174 end)
94175end
···105186 max_entries = YB_OPTS.max_entries
106187107188 vim.api.nvim_create_user_command("YankBankClearDB", function()
108108- data:remove()
189189+ data:drop()
109190 YB_YANKS = {}
110191 YB_REG_TYPES = {}
111192 end, {})
+16
lua/yankbank/utils.lua
···11+local M = {}
22+33+--- get the last zero entry in a table
44+---
55+---@param t table
66+---@return integer?
77+function M.last_zero_entry(t)
88+ for i = #t, 1, -1 do
99+ if t[i] == 0 then
1010+ return i
1111+ end
1212+ end
1313+ return nil
1414+end
1515+1616+return M