Created
May 8, 2025 11:18
-
-
Save jasonpott/b150942ba84a081d1898f42d334bdd85 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local snacks = require("snacks") | |
| local Path = require("plenary.path") | |
| local scan = require("plenary.scandir") | |
| local loop = vim.loop | |
| local utils = {} | |
| utils.file_present = function(table, filename) | |
| for _, file in pairs(table) do | |
| if file.name == filename then | |
| return true | |
| end | |
| end | |
| return false | |
| end | |
| utils.construct_case_insensitive_pattern = function(key) | |
| local pattern = "" | |
| for char in key:gmatch(".") do | |
| if char:match("%a") then | |
| pattern = pattern .. "[" .. string.lower(char) .. string.upper(char) .. "]" | |
| else | |
| pattern = pattern .. char | |
| end | |
| end | |
| return pattern | |
| end | |
| utils.fileExists = function(file) | |
| return vim.fn.empty(vim.fn.glob(file)) == 0 | |
| end | |
| utils.trimWhitespace = function(str) | |
| return str:match("^%s*(.-)%s*$") | |
| end | |
| local M = {} | |
| local config = { | |
| search_keys = { "author", "year", "title" }, | |
| citation_format = "{{author}} ({{year}}), {{title}}.", | |
| citation_trim_firstname = true, | |
| citation_max_auth = 2, | |
| format_string = "@%s", | |
| bib_dirs = { "." }, | |
| wrap = false, | |
| } | |
| local files, files_initialized = {}, false | |
| local function get_bib_files(dir) | |
| scan.scan_dir(dir, { | |
| depth = 1, | |
| search_pattern = ".*%.bib", | |
| on_insert = function(file) | |
| local p = Path:new(file):absolute() | |
| if not utils.file_present(files, p) then | |
| table.insert(files, { name = p, mtime = 0, entries = {} }) | |
| end | |
| end, | |
| }) | |
| end | |
| local function init_files() | |
| for _, dir in ipairs(config.bib_dirs) do | |
| get_bib_files(dir) | |
| end | |
| end | |
| local function read_bib_file(file) | |
| print("Attempting to read BibTeX file: " .. file) | |
| local labels, contents, search_relevants = {}, {}, {} | |
| local p = Path:new(file) | |
| if not p:exists() then | |
| print("File does not exist: " .. file) | |
| return {}, {}, {} | |
| end | |
| local data = p:read():gsub("\r", "") | |
| while true do | |
| local entry = data:match("@%w*%s*%b{}") | |
| if not entry then | |
| break | |
| end | |
| local label = entry:match("{%s*([^,]+),") | |
| if label then | |
| label = vim.trim(label) | |
| local lines = vim.split(entry, "\n") | |
| local valid_entry = false | |
| search_relevants[label] = { label = label } | |
| for _, key in ipairs(config.search_keys) do | |
| local key_pat = utils.construct_case_insensitive_pattern(key) | |
| local val = entry:match(key_pat .. '%s*=%s*[%b{}"]') | |
| if val then | |
| val = val:gsub('["{}]', ""):gsub("%s+", " "):match("^%s*(.-)%s*$") | |
| search_relevants[label][key] = tostring(val or "") | |
| valid_entry = true | |
| else | |
| search_relevants[label][key] = "" -- ensure fallback | |
| end | |
| end | |
| if valid_entry then | |
| table.insert(labels, label) | |
| contents[label] = lines | |
| end | |
| end | |
| data = data:sub(#entry + 2) | |
| end | |
| return labels, contents, search_relevants | |
| end | |
| local function load_entries() | |
| if not files_initialized then | |
| init_files() | |
| files_initialized = true | |
| end | |
| local results = {} | |
| for _, file in ipairs(files) do | |
| local stat = loop.fs_stat(file.name) | |
| local mtime = stat and stat.mtime.sec or 0 | |
| if mtime ~= file.mtime then | |
| file.entries = {} | |
| local labels, contents, search_relevants = read_bib_file(file.name) | |
| for _, label in ipairs(labels) do | |
| local entry = { | |
| name = label or "Unknown", | |
| content = contents[label] or {}, | |
| search_keys = search_relevants[label] or {}, | |
| } | |
| table.insert(results, entry) | |
| table.insert(file.entries, entry) | |
| end | |
| file.mtime = mtime | |
| else | |
| for _, e in ipairs(file.entries) do | |
| table.insert(results, e) | |
| end | |
| end | |
| end | |
| return results | |
| end | |
| local function build_items(entries) | |
| local items = {} | |
| if #entries == 0 then | |
| vim.notify("No BibTeX entries found.", vim.log.levels.WARN) | |
| return {} | |
| end | |
| for _, entry in ipairs(entries) do | |
| local author = tostring(entry.search_keys["author"] or "Unknown") | |
| local year = tostring(entry.search_keys["year"] or "n.d.") | |
| local title = tostring(entry.search_keys["title"] or "No title") | |
| local display = string.format("%s (%s), %s", author, year, title) | |
| local search = table.concat({ author, year, title }, " ") | |
| display = tostring(display) | |
| search = tostring(search) | |
| table.insert(items, { | |
| label = display, | |
| search = search, | |
| value = entry, | |
| }) | |
| end | |
| return items | |
| end | |
| local function insert_text(lines) | |
| local mode = vim.api.nvim_get_mode().mode | |
| if mode == "i" then | |
| vim.api.nvim_put(lines, "", false, true) | |
| vim.api.nvim_feedkeys("a", "n", true) | |
| else | |
| vim.api.nvim_put(lines, "", true, true) | |
| end | |
| end | |
| function M.setup(user_config) | |
| config = vim.tbl_deep_extend("force", config, user_config or {}) | |
| end | |
| function M.bibtex_picker() | |
| local entries = load_entries() | |
| local items = build_items(entries) | |
| if #items == 0 then | |
| return | |
| end | |
| snacks.picker({ | |
| title = "BibTeX References", | |
| items = items, | |
| preview = function(item) | |
| return table.concat(item.value.content, "\n") | |
| end, | |
| on_select = function(item) | |
| local cite = string.format(config.format_string, item.value.name) | |
| insert_text({ cite }) | |
| end, | |
| on_multi_select = function(selected) | |
| local citekeys = {} | |
| for _, item in ipairs(selected) do | |
| table.insert(citekeys, string.format(config.format_string, item.value.name)) | |
| end | |
| insert_text({ table.concat(citekeys, "; ") }) | |
| end, | |
| keymap = { | |
| ["<C-e>"] = function(item) | |
| insert_text(item.value.content) | |
| end, | |
| }, | |
| }) | |
| end | |
| return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment