Skip to content

Instantly share code, notes, and snippets.

@jasonpott
Created May 8, 2025 11:18
Show Gist options
  • Select an option

  • Save jasonpott/b150942ba84a081d1898f42d334bdd85 to your computer and use it in GitHub Desktop.

Select an option

Save jasonpott/b150942ba84a081d1898f42d334bdd85 to your computer and use it in GitHub Desktop.
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