Skip to content

urtzienriquez/citeref.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

citeref-logo-compact

GitHub release (latest by date) Tests

A Neovim plugin for inserting citations (from .bib files) and cross-references (to R, Python, and Julia code chunks in Quarto / R Markdown / Rnoweb documents, and LaTeX \label{} inside figure/table environments).

citeref.nvim lets you search your BibTeX library and insert formatted citations without leaving Neovim, and browse the named code chunks and LaTeX labels in your documents to insert figure and table cross-references. It works with your existing fuzzy picker and/or completion plugin — no new UI to learn.

Citations are sourced from .bib files: any file in your working directory is picked up automatically, and you can point citeref at a global library too. A fuzzy picker (fzf-lua, telescope, snacks, mini.pick) lets you search by key, title, author, or journal with a live preview; a completion backend (blink.cmp, nvim-cmp) offers the same entries inline as you type @ or \*cite*{.

Cross-references are generated by scanning the named code chunks and LaTeX \label{} definitions in the current buffer and its sibling .rmd/.qmd/.rnw/.tex files. For code chunks, supported languages are R, Python, and Julia — all three are recognised in both R Markdown and Quarto documents; R chunks are additionally detected in Rnoweb (.rnw) documents using the <<...>>= syntax. LaTeX \label is detected inside figure, figure*, table, and table* environments, and the correct environment type (fig/tab) is automatically assigned so labels appear in the right picker. Select a label and citeref inserts the right reference syntax for your document type — \@ref(label) / \@ref(fig:label) for R Markdown, @label for Quarto's native cross-reference format, or \ref{label} for LaTeX / Rnoweb.

Contributions are very welcomed! See Contributing


Requirements

  • One picker or completion backend (required — no auto-detection):

The backend option is required. citeref will warn on startup if it is not set.


How it works

citeref self-activates via a FileType autocommand — setup() is optional for attachment, but required to set a backend. When you open a supported filetype, the plugin attaches to that buffer and sets buffer-local keymaps. No external modules are loaded at this point.

The backend is loaded lazily on your first keypress, not at startup. Picker backends (fzf, telescope, snacks, minipick) work in both insert and normal mode. Completion backends (blink, cmp) provide a menu and work in insert mode only — normal-mode keymaps will warn if a picker backend is not active.

Without setup(), only *.bib files in the current working directory are used for citations. Set bib_files in setup() to include a global library.


Installation

Neovim native package manager
vim.pack.add({
  'https://github.com/urtzienriquez/citeref.nvim',
})

require("citeref").setup({
  backend   = "fzf",   -- required: "fzf" | "telescope" | "snacks" | "minipick" | "blink" | "cmp"
  bib_files = { "/path/to/your/library.bib" },
})
lazy.nvim
{
  "urtzienriquez/citeref.nvim",
  ft = { "markdown", "rmd", "quarto", "rnoweb", "pandoc", "tex", "latex" },
  config = function()
    require("citeref").setup({
      backend   = "fzf",   -- required: "fzf" | "telescope" | "snacks" | "minipick" | "blink" | "cmp"
      bib_files = { "/path/to/your/library.bib" },
    })
  end,
}

blink.cmp source

Register citeref as a blink.cmp provider. Note the module path points to the blink backend:

-- in your blink.cmp config:
sources = {
  default = { "lsp", "path", "snippets", "buffer" },
  providers = {
    citeref = {
      name   = "citeref",
      module = "citeref.backends.blink",
    },
  },
  per_filetype = {
    markdown = { inherit_defaults = true, "citeref" },
    rmd      = { inherit_defaults = true, "citeref" },
    quarto   = { inherit_defaults = true, "citeref" },
    tex      = { inherit_defaults = true, "citeref" },
    latex    = { inherit_defaults = true, "citeref" },
  },
},

nvim-cmp source

-- in your nvim-cmp config:
-- Register the source before cmp.setup() so nvim-cmp knows about it:
require("citeref.backends.cmp").register()

sources = cmp.config.sources({
  { name = "nvim_lsp" },
  { name = "citeref" },
})

Completion triggers

When using a completion backend (blink or cmp), citeref activates automatically based on what you type — no keymap needed. The full set of triggers is:

What you type Filetype Items offered
@ R Markdown / Markdown Citations only
@ Quarto Citations and chunk crossrefs
\cite{ / \citep{ / \parencite{ etc. any Citations (key only, to complete the {})
\ref{ tex / latex All crossref labels (both fig and tab)
\@ref( R Markdown All chunk labels, inserting fig:label) or tab:label)
\@ref(fig: R Markdown Figure chunk labels only, inserting label)
\@ref(tab: R Markdown Table chunk labels only, inserting label)

In Quarto, @ is the universal trigger for both citations and crossrefs — the menu offers both together and you filter by typing. The \@ref( trigger is R Markdown only because Quarto does not use that syntax.

The keymaps (<C-a>f, <C-a>t, etc.) are still available with completion backends as an alternative way to open the menu pre-filtered to a specific type.


Configuration (optional)

All options have defaults. Call setup() to set a backend and override anything else.

require("citeref").setup({

  -- REQUIRED. No auto-detection — set one explicitly:
  --   "fzf"       → fzf-lua: full picker with preview, insert + normal mode
  --   "telescope" → telescope.nvim: full picker with preview, insert + normal mode
  --   "snacks"    → snacks.nvim: full picker with preview, insert + normal mode
  --   "minipick"  → mini.pick: full picker with preview, insert + normal mode
  --   "blink"     → blink.cmp: completion menu, insert mode only
  --   "cmp"       → nvim-cmp: completion menu, insert mode only
  backend = "fzf",

  -- Filetypes where citeref activates.
  filetypes = {
    "markdown", "rmd", "quarto", "rnoweb", "pandoc", "tex", "latex",
  },

  -- .bib files for citations.
  -- cwd *.bib files are always included. This adds more sources on top.
  -- Accepts:
  --   string[]       → explicit paths (~ expanded; missing files warned once)
  --   fun():string[] → function called each time a picker opens (dynamic)
  bib_files = { "/path/to/your/library.bib" },

  -- Default LaTeX citation command used when "default" is selected from the
  -- format prompt. A vim.ui.select dialog opens first to let you pick a
  -- format; press <C-l> inside the picker to cycle through all formats.
  -- Valid values: cite | citep | citet | citeauthor | citeyear | citealt |
  --               textcite | parencite | footcite | autocite | nocite
  default_latex_format = "cite",

  -- Default MyST citation role used when "default" is selected from the
  -- format prompt. A vim.ui.select dialog opens first to let you pick a
  -- format; press <C-l> inside the picker to cycle between {cite:p} and {cite:t}.
  -- Valid values: "cite:p" | "cite:t"
  default_myst_format = "cite:p",

  keymaps = {
    -- Set to false to disable all default keymaps.
    enabled = true,

    -- Each action has an insert-mode (_i) and normal-mode (_n) variant.
    -- Set any to false to disable that individual mapping.
    -- Normal-mode keymaps require a picker backend; they warn otherwise.
    cite_markdown_i   = "<C-a>m",      -- insert @key              (insert mode)
    cite_markdown_n   = "<leader>am",  -- insert @key              (normal mode)
    cite_latex_i      = "<C-a>l",      -- insert LaTeX citation — format prompt first (insert mode)
    cite_latex_n      = "<leader>al",  -- insert LaTeX citation — format prompt first (normal mode)
    cite_myst_i       = "<C-a>s",      -- insert MyST citation — format prompt first  (insert mode)
    cite_myst_n       = "<leader>as",  -- insert MyST citation — format prompt first  (normal mode)
    cite_replace_n    = "<leader>ar",  -- replace key under cursor (normal only)
    crossref_figure_i = "<C-a>f",      -- crossref figure          (insert mode)
    crossref_figure_n = "<leader>af",  -- crossref figure          (normal mode)
    crossref_table_i  = "<C-a>t",      -- crossref table           (insert mode)
    crossref_table_n  = "<leader>at",  -- crossref table           (normal mode)
  },

  -- Picker appearance.
  -- `layout` meaning depends on the backend:
  --   fzf / telescope → "vertical" | "horizontal"
  --   snacks          → any snacks preset name:
  --                     "default" | "vertical" | "horizontal" | "telescope" |
  --                     "ivy" | "ivy_split" | "select" | "sidebar" | "vscode"
  --   minipick        → not used (mini.pick layout is controlled via MiniPick.config)
  -- `preview_size` is used by fzf and telescope only.
  -- `rnoweb_labels` controls which crossref targets appear for .Rnw files:
  --   "all"      (default) — code chunks + LaTeX \label{} inside figure/table
  --   "tex_only"           — only LaTeX \label{} inside figure/table (skip code chunks)
  picker = {
    layout        = "vertical",
    preview_size  = "50%",
    rnoweb_labels = "all",
  },
})

Overriding individual keymaps

require("citeref").setup({
  backend  = "fzf",
  keymaps  = { enabled = false },
})

vim.api.nvim_create_autocmd("FileType", {
  pattern  = { "markdown", "quarto", "rmd" },
  callback = function()
    local cr = require("citeref")
    vim.keymap.set("i", "<M-c>", cr.cite_markdown,   { buffer = true })
    vim.keymap.set("i", "<M-r>", cr.crossref_figure, { buffer = true })
  end,
})

Default keymaps

Mode Key Action Requires
insert <C-a>m Insert citation @key any backend
normal <leader>am Insert citation @key picker backend
insert <C-a>l Insert LaTeX citation any backend
normal <leader>al Insert LaTeX citation picker backend
insert <C-a>s Insert MyST citation picker backend
normal <leader>as Insert MyST citation picker backend
normal <leader>ar Replace citation under cursor picker backend
insert <C-a>f Insert figure crossref any backend
normal <leader>af Insert figure crossref picker backend
insert <C-a>t Insert table crossref any backend
normal <leader>at Insert table crossref picker backend

The inserted crossref syntax depends on the filetype — see Cross-references below.

Normal-mode keymaps with a completion backend show a warning — there is no picker equivalent for normal mode without fzf-lua, telescope, snacks, or mini.pick.


Citation format selection

When using a picker backend, both the LaTeX and MyST citation pickers first show a vim.ui.select dialog to choose the citation format. This lets you quickly pick a specific format without cycling through the list.

The first option is always default — selecting it (or pressing <Esc>) opens the picker with your configured default format:

Citation type Config option Default
LaTeX default_latex_format \cite{}
MyST default_myst_format {cite:p}

Selecting any other format opens the picker with that format active.

Format cycling inside the picker

Once the picker is open, press <C-l> to cycle through all available formats. The current format is shown in the picker title; a notification confirms each cycle.

LaTeX formats

Command Output
\cite{} \cite{key}
\citep{} \citep{key}
\citet{} \citet{key}
\citeauthor{} \citeauthor{key}
\citeyear{} \citeyear{key}
\citealt{} \citealt{key}
\textcite{} \textcite{key}
\parencite{} \parencite{key}
\footcite{} \footcite{key}
\autocite{} \autocite{key}
\nocite{} \nocite{key}

MyST formats

Command Output
{cite:p} {cite:p}key``
{cite:t} {cite:t}key``

Setting the default format

Set the default in setup():

require("citeref").setup({
  backend              = "fzf",
  default_latex_format = "citep",
  default_myst_format  = "cite:t",
})

Or change it on the fly at any time:

require("citeref").set_latex_format("citep")
require("citeref").set_myst_format("cite:t")

These update the session default directly without triggering the backend configuration warning — useful for switching formats per-document.

Completion backends (blink, cmp) trigger on @ for markdown and \cite{ for LaTeX. They do not support format selection or cycling — the format is determined by what you type.


Bib file resolution

Without setup(), citeref scans only *.bib files in the current working directory. If none are found it warns once when you trigger a citation.

With setup({ bib_files = { ... } }), the configured files are merged with any cwd *.bib files (duplicates removed). Missing configured files produce a one-time warning.

-- Static global library + any project-local .bib automatically included
require("citeref").setup({
  backend   = "fzf",
  bib_files = { "/path/to/your/library.bib" },
})

-- Dynamic: re-evaluated on every picker open
require("citeref").setup({
  backend   = "fzf",
  bib_files = function()
    return vim.fn.globpath(vim.fn.getcwd(), "**/*.bib", false, true)
  end,
})

Cross-references

The crossref pickers scan:

  1. The current buffer
  2. All *.{rmd,Rmd,qmd,Qmd,rnw,Rnw} files in the same directory

For LaTeX (.tex) buffers, they scan the current buffer plus all sibling .tex files for \label{...} definitions inside figure, figure*, table, and table* environments. The environment type is tracked automatically — figure labels appear only in the figure picker, table labels only in the table picker.

Supported sources

citeref detects cross-reference targets from two sources:

Code chunks in R, Python, and Julia — all three are detected in both R Markdown and Quarto documents. Labels always come from the #| label: YAML option for Python and Julia; R additionally supports the legacy inline label on the fence line. Rnoweb (.rnw) documents are also supported — R chunks use the <<...>>= syntax and labels are extracted from the chunk header.

LaTeX \label{...} inside figure, figure*, table, and table* environments — the environment type is tracked automatically as the parser walks the \begin/\end stack, so figure labels only appear in the figure picker and table labels only in the table picker. Commented-out environments (% \begin{figure}) are ignored. Labels outside any recognised environment are skipped.

Chunk label syntax

R Markdown / knitr inline label (R only — label on the fence line):

```{r myplot, fig.cap="My caption"}
# referenced as \@ref(fig:myplot)
```

Quarto YAML options (all languages — label as a #| comment inside the chunk):

```{r}
#| label: myplot
#| fig-cap: "My caption"
```

```{python}
#| label: fig-scatter
#| fig-cap: "A scatter plot"
import matplotlib.pyplot as plt
plt.plot(x, y)
plt.show()
```

```{julia}
#| label: fig-lineplot
#| fig-cap: "A line plot"
using Plots
plot([1, 2, 3], [1, 4, 9])
```

All three styles are detected and shown together in the picker, regardless of which file type you are editing.

Rnoweb (.rnw) chunk syntax

Rnw chunks use <<...>>= headers. The label can be a standalone name, an explicit label= option, or the first token before options:

<<my-plot>>=
plot(1:10)
@

<<label=my-table>>=
knitr::kable(head(mtcars))
@

<<echo=FALSE, label=fig-line>>=
plot(1:10, type="l")
@

<<setup, include=FALSE>>=
knitr::opts_chunk$set(echo = TRUE)
@

<<>>=
# unnamed — no label
x <- 1
@

LaTeX label syntax

Labels inside figure/figure* and table/table* environments are detected in any filetype:

\begin{figure}[htbp]
  \centering
  \includegraphics{birds.jpg}
  \caption{The birds}
  \label{fig:birds}
\end{figure}

\begin{table}
  \centering
  \begin{tabular}{cc}
    A & B \\
  \end{tabular}
  \caption{My table}
  \label{mytab}
\end{table}

The environment type is tracked automatically: fig:birds appears in the figure picker, mytab in the table picker. Using a fig:/tab: prefix is optional — citeref preserves the label as-is. When inserting a reference, the prefix is never added or stripped:

Inserted syntax

The reference format inserted on selection depends on the filetype of the buffer you are editing:

Filetype Source Figure crossref Table crossref
quarto (.qmd) code chunk @fig-example @tbl-example
quarto (.qmd) LaTeX \label{} @label (echoed as-is) @label (echoed as-is)
rmd, markdown code chunk \@ref(fig:example) \@ref(tab:example)
rmd, markdown LaTeX \label{} \@ref(label) (echoed as-is) \@ref(label) (echoed as-is)
rnoweb (.rnw) code chunk \ref{example} \ref{example}
rnoweb (.rnw) LaTeX \label{} \ref{label} (echoed as-is) \ref{label} (echoed as-is)
tex, latex LaTeX \label{} \ref{label} (echoed as-is) \ref{label} (echoed as-is)

LaTeX \label{}: the label is echoed verbatim — if you write \label{fig:myfig}, the reference becomes \ref{fig:myfig} (tex / rnoweb) or \@ref(fig:myfig) (rmd). If you write \label{myfig}, the reference becomes \ref{myfig} or \@ref(myfig) with no added prefix. Using fig: / tab: prefixes in your labels is not required but is a recommended convention to keep your labels organised — citeref preserves whatever prefix you use without adding or removing anything.

For Quarto documents, citeref inserts @label and leaves the prefix (fig-, tbl-) entirely up to you — Quarto requires that labels follow the fig-* / tbl-* naming convention for cross-referencing to work, but enforcing that is your responsibility, not the plugin's.

Unnamed chunks appear in the list with a warning label; selecting one produces a notification telling you to add a label first.

```{python}
# unnamed — add #| label: to use in a cross-reference
x = 42
```

Extending citeref with a custom backend

citeref uses a backend registry. You can register any table as a backend — from your own config, from a separate plugin, or to override a built-in.

require("citeref").register_backend("my_picker", {
  -- Called for citation insertion (picker backends)
  pick_citation = function(format, entries, ctx, cmd)
    -- format:  "markdown" | "latex" | "myst"
    -- entries: CiterefEntry[]  (key, title, author, year, journaltitle, abstract)
    -- ctx:     saved cursor context from util.save_context()
    -- cmd:     optional — initial format command for latex/myst, or nil for default
  end,

  -- Called for crossref insertion (picker backends)
  pick_crossref = function(ref_type, chunks, ctx)
    -- ref_type: "fig" | "tab"
    -- chunks:   CiterefChunk[]  (label, display, line, file, is_current, header)
    -- ctx:      also carries ctx.bufnr — the buffer being edited
  end,

  -- Called for replacing a citation key under the cursor (picker backends)
  replace = function(entries, info)
    -- info: { key, start_col, end_col, style ("markdown"|"latex"), ... }
  end,

  -- Completion backends only: register your source with the engine once
  register = function() end,

  -- Completion backends only: open the menu in a specific mode
  show = function(mode, format)
    -- mode:   "citation" | "crossref_fig" | "crossref_tab" | "all"
    -- format: "markdown" | "latex"
  end,
})

Only implement the functions your backend supports. citeref checks for nil before calling and warns if a required function is missing. Built-in backends live in lua/citeref/backends/ and are a good reference for implementation.

The shared parsers are available for reuse:

local parse = require("citeref.parse")

parse.load_entries()                        -- resolve bib files + parse → CiterefEntry[]
parse.load_chunks()                         -- scan current buf + siblings → CiterefChunk[]
parse.entry_display(entry)                  -- "key │ title │ author"
parse.entry_preview(entry)                  -- multi-line preview string
parse.format_citation(keys)                 -- "@key1; @key2" (markdown only)
parse.format_crossref(ref_type, label, bufnr) -- filetype-aware: "@label" or "\@ref(fig:label)"
parse.citation_under_cursor()               -- detect citation at cursor → info table or nil

Note: for LaTeX citations, use "\\cite{" .. cmd .. "}{" .. table.concat(keys, ", ") .. "}" directly — format_citation is markdown-only. The available commands are defined in lua/citeref/latex_formats.lua.


Architecture

lua/citeref/
  init.lua              Public API, keymap setup, buffer attach, register_backend()
  config.lua            Options, defaults, validation
  parse.lua             Bib parser, chunk parser (R/Python/Julia), shared display helpers
  util.lua              Cursor context save/restore, text insertion
  latex_formats.lua     LaTeX citation command definitions (single source of truth)
  backends/
    init.lua            Backend registry and lazy loader
    fzf.lua             fzf-lua picker (citations, crossrefs, replace)
    telescope.lua       telescope.nvim picker (citations, crossrefs, replace)
    snacks.lua          snacks.nvim picker (citations, crossrefs, replace)
    minipick.lua        mini.pick picker (citations, crossrefs, replace)
    blink.lua           blink.cmp completion source
    cmp.lua             nvim-cmp completion source
plugin/
  citeref.lua           FileType autocommand (startup entry point)

Startup cost

citeref is designed to have zero startup impact:

  • plugin/citeref.lua only registers a FileType autocmd — no modules load.
  • On buffer attach, only citeref.config and citeref.init load (tiny pure-Lua, no external dependencies).
  • The backend module (citeref.backends.fzf etc.) loads on your first keypress only.

Programmatic API

local citeref = require("citeref")

citeref.cite_markdown()        -- insert @key
citeref.cite_latex()           -- insert \cite{key}
citeref.cite_replace()         -- replace citation key under cursor (picker backends only)
citeref.crossref_figure()      -- insert figure crossref (format depends on filetype)
citeref.crossref_table()       -- insert table crossref  (format depends on filetype)
citeref.set_latex_format(cmd)  -- change default LaTeX format on the fly (e.g. "citep")
citeref.set_myst_format(cmd)   -- change default MyST format on the fly  (e.g. "cite:t")

citeref.register_backend(name, backend)  -- register a custom backend

citeref.debug()                -- print backend, attachment status, and active keymaps

Contributing

Contributions of any kind are very welcome — bug reports, feature suggestions, documentation improvements, and especially code. If you have an idea for a new backend, a fix, or a quality-of-life improvement, please open an issue or pull request. This plugin is small and the codebase is meant to be easy to navigate, so diving in should be straightforward.

If you want to contribute code, the Architecture section at the bottom of this README is a good starting point for orientation. Custom backends are particularly easy to add without touching core files — see Extending citeref with a custom backend.


Similar projects

  • zotcite — the most feature-rich option if you use Zotero. Queries the Zotero database directly (no .bib export needed), provides omnicompletion, opens PDF attachments, and can extract annotations and notes. Requires Python 3, sqlite3, and nvim-treesitter. Supports Markdown, Quarto, RMarkdown, LaTeX, Rnoweb, Typst, and vimwiki.

  • telescope-zotero.nvim — a Telescope extension that browses your Zotero library and inserts the selected reference into a .bib file. Requires Zotero, Better BibTeX, and sqlite.lua. The intended workflow keeps citation insertion (via this plugin) separate from in-document completion (handled by a companion cmp source).

  • zotex.nvim — an nvim-cmp completion source that imports references directly from the Zotero SQLite database. Lightweight and focused: configure it as a cmp source for your filetypes and it will surface Zotero entries as you type. Requires sqlite.lua.


License

GNU General Public License v3.0 — see LICENSE.

About

A Neovim plugin for inserting citations from .bib files and cross-references to R/Quarto code chunks.

Resources

License

Stars

Watchers

Forks

Contributors

Languages