Neovim plugin improving access to clipboard history (mirror)
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