···11--- clipboard.lua
21local M = {}
3243-- import persistence module
+3-11
lua/yankbank/data.lua
···11--- data.lua
21local M = {}
3243---reformat yanks table for popup
···3635 for j, line in ipairs(yank_lines) do
3736 if j == 1 then
3837 -- Format the line number with uniform spacing
3939- local lineNumber =
4040- string.format("%" .. max_digits .. "d: ", yank_num)
3838+ local lineNumber = string.format("%" .. max_digits .. "d: ", yank_num)
4139 line = line:sub(leading_space_length + 1)
4240 table.insert(display_lines, lineNumber .. line)
4341 else
4442 -- Remove the same amount of leading whitespace as on the first line
4543 line = line:sub(leading_space_length + 1)
4644 -- Use spaces equal to the line number's reserved space to align subsequent lines
4747- table.insert(
4848- display_lines,
4949- string.rep(" ", max_digits + 2) .. line
5050- )
4545+ table.insert(display_lines, string.rep(" ", max_digits + 2) .. line)
5146 end
5247 table.insert(line_yank_map, i)
5348 end
···5550 if i < #yanks then
5651 -- Add a visual separator between yanks, aligned with the yank content
5752 if sep ~= "" then
5858- table.insert(
5959- display_lines,
6060- string.rep(" ", max_digits + 2) .. sep
6161- )
5353+ table.insert(display_lines, string.rep(" ", max_digits + 2) .. sep)
6254 end
6355 table.insert(line_yank_map, false)
6456 end
-1
lua/yankbank/helpers.lua
···11--- helpers.lua
21local M = {}
3243-- navigate to the next numbered item
+5-8
lua/yankbank/init.lua
···11--- init.lua
21local M = {}
3243-- local imports
···2322 persist_path = plugin_path .. "bank.txt",
2423}
25242626--- wrapper function for main plugin functionality
2525+--- wrapper function for main plugin functionality
2726---@param opts table
2727+--- TODO: read from persistent database if sql persist is set (allow multi-session sync)
2828local function show_yank_bank(opts)
2929 -- create and fill buffer
3030- local bufnr, display_lines, line_yank_map =
3131- menu.create_and_fill_buffer(yanks, reg_types, opts)
3030+ local bufnr, display_lines, line_yank_map = menu.create_and_fill_buffer(yanks, reg_types, opts)
32313332 -- handle empty bank case
3433 if not bufnr or not display_lines or not line_yank_map then
···4544 -- merge opts with default options table
4645 opts = vim.tbl_deep_extend("force", default_opts, opts or {})
47464848- -- enable persistence based on opts
4949- -- (needs to be called before autocmd setup)
5050- persistence.setup(yanks, reg_types, opts)
5151- -- yanks, reg_types = persistence.setup(yanks, reg_types, opts)
4747+ -- enable persistence based on opts (needs to be called before autocmd setup)
4848+ yanks, reg_types = persistence.setup(opts)
52495350 -- create clipboard autocmds
5451 clipboard.setup_yank_autocmd(yanks, reg_types, opts)
+4-11
lua/yankbank/menu.lua
···11--- menu.lua
21local M = {}
3243-- import clipboard functions
···6160 -- define buffer window width and height based on number of columns
6261 -- FIX: long enough entries will cause window to go below end of screen
6362 -- FIX: wrapping long lines will cause entries below to not show in menu (requires scrolling to see)
6464- local width =
6565- math.min(max_width, vim.api.nvim_get_option_value("columns", {}) - 4)
6363+ local width = math.min(max_width, vim.api.nvim_get_option_value("columns", {}) - 4)
6664 local height = math.min(
6765 display_lines and #display_lines or 1,
6866 vim.api.nvim_get_option_value("lines", {}) - 10
···7371 relative = "editor",
7472 width = width,
7573 height = height,
7676- col = math.floor(
7777- (vim.api.nvim_get_option_value("columns", {}) - width) / 2
7878- ),
7979- row = math.floor(
8080- (vim.api.nvim_get_option_value("lines", {}) - height) / 2
8181- ),
7474+ col = math.floor((vim.api.nvim_get_option_value("columns", {}) - width) / 2),
7575+ row = math.floor((vim.api.nvim_get_option_value("lines", {}) - height) / 2),
8276 border = "rounded",
8377 style = "minimal",
8478 })
···118112 local k = vim.tbl_deep_extend("force", default_keymaps, opts.keymaps or {})
119113120114 -- merge default and options keymap tables
121121- opts.registers =
122122- vim.tbl_deep_extend("force", default_registers, opts.registers or {})
115115+ opts.registers = vim.tbl_deep_extend("force", default_registers, opts.registers or {})
123116124117 -- check table for number behavior option (prefix or jump, default to prefix)
125118 opts.num_behavior = opts.num_behavior or "prefix"
+6-21
lua/yankbank/persistence.lua
···11--- persistence.lua
21local M = {}
3243local persistence = {}
55-local db = nil
6475---add entry from bank to
86---@param entry string|table
···119function M.add_entry(entry, reg_type, opts)
1210 if not opts.persist_type then
1311 return
1414- elseif opts.persist_type == "file" then
1515- persistence.add_to_bankfile(opts.persist_path, entry, reg_type)
1612 elseif opts.persist_type == "sqlite" then
1717- persistence.add_to_yanktable(db, entry, reg_type)
1313+ persistence:insert_yank(entry, reg_type)
1814 end
1915end
20162117---initialize bank persistence
2222----@param yanks table
2323----@param reg_types table
2418---@param opts table
2519---@return table
2620---@return table
2727-function M.setup(yanks, reg_types, opts)
2121+function M.setup(opts)
2822 if not opts.persist_type then
2923 return {}, {}
3030- elseif opts.persist_type == "file" then
3131- persistence = require("yankbank.persistence.file")
3232- return persistence.setup_persistence(
3333- opts.persist_path,
3434- opts.max_entries,
3535- yanks,
3636- reg_types
3737- )
3824 elseif opts.persist_type == "sqlite" then
3939- persistence = require("yankbank.persistence.sql")
4040- db = persistence.init_db(yanks, reg_types, opts.persist_path)
4141- return yanks, reg_types
2525+ persistence = require("yankbank.persistence.sql").setup(opts)
2626+ return persistence:get_bank()
2727+ else
2828+ return {}, {}
4229 end
4343-4444- return {}, {}
4530end
46314732return M
-238
lua/yankbank/persistence/file.lua
···11--- persistence/file.lua
22-local M = {}
33-44-local n_entries = 0
55-local m_entries = 10
66-77----function that checks if a file exists
88----@param file string: file path
99----@return boolean
1010-local function file_exists(file)
1111- local f = io.open(file, "rb")
1212- if f then
1313- f:close()
1414- end
1515- return f ~= nil
1616-end
1717-1818----function that reads all lines of file into a table
1919----@param file string: file path
2020----@return table
2121-local function read_lines(file)
2222- local f, err = io.open(file)
2323- if not f then
2424- error("Error opening file: " .. err)
2525- end
2626- local lines = {}
2727- for line in f:lines() do
2828- lines[#lines + 1] = line
2929- end
3030- f:close()
3131- return lines
3232-end
3333-3434----check first line from file for presence of yankbank list header.
3535----if it exists, populate current number of entries.
3636----@param line string
3737----@return boolean
3838-local function check_for_header(line)
3939- local n = string.match(line, "<YANKBANK_LIST:(%d+)>")
4040- if n then
4141- n_entries = tonumber(n, 10)
4242- return true
4343- end
4444- return false
4545-end
4646-4747----function that checks for the presence of a yankbank header on a given line.
4848----returns t/f and index, length for entries that exist
4949----@param line string: line from file being checked
5050----@return table|nil
5151-local function check_for_entry(line)
5252- local i, l, rt = string.match(line, "<YANKBANK_ENTRY:(%d+),(%d+),(%a+)>")
5353- if i then
5454- return {
5555- index = tonumber(i),
5656- length = tonumber(l),
5757- reg_type = rt,
5858- }
5959- end
6060-end
6161-6262----get line count of a string
6363----@param str string
6464----@return integer
6565-local function get_line_count(str)
6666- local lines = 1
6767- for i = 1, #str do
6868- local c = str:sub(i, i)
6969- if c == "\n" then
7070- lines = lines + 1
7171- end
7272- end
7373- return lines
7474-end
7575-7676----function that reads a yankbank entry from an index to an offset.
7777----@param i integer: starting index
7878----@param offset integer: stopping point = i+offset
7979----@param lines table: file contents
8080----@return table: yankbank entry in string table form
8181-local function read_entry(i, offset, lines)
8282- local entry = {}
8383- for j = i, i + offset - 1 do
8484- entry[#entry + 1] = lines[j]
8585- end
8686- -- handle extra newline added to end of entry in bank file
8787- if #entry > 1 then
8888- table.remove(entry)
8989- end
9090- return entry
9191-end
9292-9393----remove entry from bankfile
9494----@param file string: bank file name
9595-local function remove_last_entry(file)
9696- local f, err = io.open(file, "r+")
9797- if not f then
9898- error("Could not open file for reading: " .. err)
9999- end
100100-101101- -- read lines from file until matching entry is found
102102- local lines = {}
103103- for line in f:lines() do
104104- if
105105- string.match(line, "<YANKBANK_ENTRY:" .. n_entries .. ",%d+,%a+>")
106106- then
107107- n_entries = n_entries - 1
108108- lines[1] = "<YANKBANK_LIST:" .. n_entries .. ">"
109109- break
110110- else
111111- lines[#lines + 1] = line
112112- end
113113- end
114114- f:close()
115115-116116- -- write to file
117117- f, err = io.open(file, "w")
118118- if not f then
119119- error("Could not open file for writing: " .. err)
120120- end
121121- for i = 1, #lines do
122122- f:write(lines[i] .. "\n")
123123- end
124124- f:close()
125125-end
126126-127127----add entry bankfile. (this function needs to be callable from outside the module)
128128----@param file string
129129----@param entry table|string
130130----@param reg_type string
131131-function M.add_to_bankfile(file, entry, reg_type)
132132- -- remove last entry if new capacity would exceed maximum
133133- if n_entries >= m_entries then
134134- remove_last_entry(file)
135135- end
136136- n_entries = n_entries + 1
137137-138138- local lines = read_lines(file)
139139- local f, err = io.open(file, "w+")
140140- if not f then
141141- error("Could not open file: " .. err)
142142- end
143143-144144- -- add list header
145145- f:write("<YANKBANK_LIST:" .. n_entries .. ">\n")
146146-147147- -- get line count of entry (special case for strings)
148148- local len = #entry
149149- if type(entry) == "string" then
150150- len = get_line_count(entry)
151151- end
152152-153153- -- write entry header
154154- f:write("<YANKBANK_ENTRY:1," .. len .. "," .. reg_type .. ">\n")
155155- -- write new entry
156156- if type(entry) == "string" then
157157- f:write(entry .. "\n")
158158- else
159159- for i = 1, #entry do
160160- f:write(entry[i] .. "\n")
161161- end
162162- end
163163-164164- -- write back previous entries
165165- for i = 2, #lines do
166166- local n, l, rt =
167167- string.match(lines[i], "<YANKBANK_ENTRY:(%d+),(%d+),(%a+)>")
168168- if n then
169169- lines[i] = "<YANKBANK_ENTRY:"
170170- .. n + 1
171171- .. ","
172172- .. l
173173- .. ","
174174- .. rt
175175- .. ">"
176176- end
177177- f:write(lines[i] .. "\n")
178178- end
179179-180180- f:close()
181181-end
182182-183183----populate yankbank with entries contained in file.
184184----@param yanks table: table to populate with yanks
185185----@param file string: yankbank persistence file
186186----@param max_entries integer: maximum number of yankbank entries
187187----@return table, table
188188-local function populate_yankbank(file, max_entries, yanks, reg_types)
189189- -- read lines from file
190190- local lines = read_lines(file)
191191- if not check_for_header(lines[1]) then
192192- print("YankBank list header not found in file...")
193193- return {}, {}
194194- end
195195-196196- -- iterate through remaining lines in file, adding entries to yankbank
197197- local i = 2
198198- while i <= #lines do
199199- local res = check_for_entry(lines[i])
200200- if res then
201201- local entry = read_entry(i + 1, res.length, lines)
202202- if res.index < max_entries then
203203- yanks[#yanks + 1] = entry
204204- reg_types[#reg_types + 1] = res.reg_type
205205- end
206206- -- skip lines that were added to entries
207207- i = i + res.length
208208- end
209209- i = i + 1
210210- end
211211- return yanks, reg_types
212212-end
213213-214214----setup function for a persistence file.
215215----should be called in plugin setup function
216216----@param file string: file path
217217----@param max_entries integer: maximum number of yankbank entries
218218----@param yanks table: table to populate with yanks
219219----@param reg_types table: table containing register types
220220----@return table, table
221221-function M.setup_persistence(file, max_entries, yanks, reg_types)
222222- -- check if file exists, otherwise create it (with header)
223223- if not file_exists(file) then
224224- print("Creating file...")
225225- local f, err = io.open(file, "w")
226226- if f then
227227- f:write("<YANKBANK_LIST:0>")
228228- f:close()
229229- else
230230- print(err)
231231- end
232232- return {}, {}
233233- end
234234- m_entries = max_entries
235235- return populate_yankbank(file, max_entries, yanks, reg_types)
236236-end
237237-238238-return M
+58-81
lua/yankbank/persistence/sql.lua
···2233local sqlite = require("sqlite.db")
4455--- TODO: yank primary key?
66--- integer tracking for table position not controlled by sqlite3
55+local dbdir = vim.fn.stdpath("data") .. "/databases"
66+local max_entries = 10
7788----create db table for yanks, PK is row id and will increment automatically
99--- @param existing_yanks table
1010--- @param reg_types table
1111--- @param uri string
1212--- @return sqlite_db
1313-function M.init_db(existing_yanks, reg_types, uri)
1414- local db = sqlite({
1515- uri = uri,
1616- })
1717- db:open()
88+---@class YankBankDB:sqlite_db
99+---@field bank sqlite_tbl
1010+local db = sqlite({
1111+ uri = dbdir .. "/yankbank.db",
1212+ bank = {
1313+ -- yanked text should be unique and be primary key
1414+ yank_text = { "text", unique = true, primary = true, required = true },
1515+ reg_type = { "text", required = true },
1616+ },
1717+})
1818+1919+---@class sqlite_tbl
2020+local data = db.bank
18211919- db:create("yanks", {
2020- id = true,
2121- yank_content = { "text", required = true },
2222- reg_type = { "text", required = true },
2323- ensure = true,
2222+--- insert yank entry into database
2323+---@param yank_text string yanked text
2424+---@param reg_type string register type
2525+function data:insert_yank(yank_text, reg_type)
2626+ -- attempt to remove entry if count > 0 (to move potential duplicate)
2727+ if self:count() > 0 then
2828+ self:remove({ yank_text = yank_text })
2929+ end
3030+3131+ -- insert entry
3232+ self:insert({
3333+ yank_text = yank_text,
3434+ reg_type = reg_type,
2435 })
2525- local status = db:status()
26362727- db:insert("yanks", { yank_content = existing_yanks, reg_type = reg_types })
3737+ -- attempt to trim database size
3838+ self:trim_size()
3939+end
28402929- if status ~= nil then
3030- print("yankbank db error: ", status.code)
4141+--- trim database size if it exceeds max_entries option
4242+function data:trim_size()
4343+ if self:count() > max_entries then
4444+ -- remove the oldest entry
4545+ self:remove({ yank_text = self:get()[1].yank_text })
3146 end
3232- -- TODO: add functionality to add existing yanks to the db table "yanks"
3333- db:close()
3434- return db
3547end
36483737--- add entry to DB
3838--- @param db sqlite_db
3939--- @param yank_content string
4040--- @param reg_type string
4141--- @return boolean
4242-function M.add_to_yanktable(db, yank_content, reg_type)
4343- db:open()
4444- db:insert("yanks", { yank_content = yank_content, reg_type = reg_type })
4545- local status = db:status()
4646- db:close()
4747- return status == nil
4949+--- get sqlite bank contents
5050+---@return table yanks, table reg_types
5151+function data:get_bank()
5252+ local yanks, reg_types = {}, {}
5353+5454+ local bank = self:get()
5555+ for _, entry in ipairs(bank) do
5656+ table.insert(yanks, 1, entry.yank_text)
5757+ table.insert(reg_types, 1, entry.reg_type)
5858+ end
5959+6060+ return yanks, reg_types
4861end
49625050--- removes entry from yanktable
5151--- @param db sqlite_db
5252--- @param yank_content string
5353--- @return boolean
5454-function M.remove_from_yanktable(db, yank_content)
5555- db:open()
5656- db:delete("yanks", { where = { yank_content = yank_content } })
5757- local status = db:status()
5858- db:close()
5959- return status == nil
6060-end
6363+-- FIX: correctly handle multiple sessions open at once
6464+-- - fetch database state each time YankBank command is called?
61656262--- returns all yanks in table sorted by recency descending
6363--- @param db sqlite_db
6464--- @return table[]
6565-function M.get_yanks(db)
6666- db:open()
6767- local ret = db:select("yanks", { order_by = { asc = "id" } })
6868- db:close()
6969- return ret
7070-end
6666+--- set up database persistence
6767+---@param opts table
6868+---@return sqlite_tbl data
6969+function M.setup(opts)
7070+ max_entries = opts.max_entries
71717272-function M.remove_by_yank_index(db, index)
7373- db:open()
7474- local ret = db:select("yanks", { order_by = { asc = "id" } })
7575- local id_to_remove = ret[index].id
7676- local del = db:delete("yanks", { where = { id = id_to_remove } })
7777- if del ~= nil then
7878- return del
7272+ if vim.fn.isdirectory(dbdir) == 0 then
7373+ vim.fn.mkdir(dbdir, "p")
7974 end
8080- local status = db:status()
8181- db:close()
8282- return status == nil
8383-end
84758585--- test function for db operations
8686-local function test_database()
8787- local test_db = M.init_db({}, {}, "/tmp/test_yankbank.db")
8888- -- print(vim.inspect(test_db))
8989- M.add_to_yanktable(test_db, "Sample Yank", "reg")
9090- print(vim.inspect(M.get_yanks(test_db)))
9191- M.add_to_yanktable(test_db, "Sample Different Yank", "reg")
9292- M.remove_from_yanktable(test_db, "Sample Different Yank")
9393- print("after SDY delete")
9494- print(vim.inspect(M.get_yanks(test_db)))
9595- M.remove_by_yank_index(test_db, 2)
9696- print("after index 2 delete")
9797- print(vim.inspect(M.get_yanks(test_db)))
7676+ return data
9877end
9999-100100-test_database()
1017810279return M