Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

[![GitHub release (latest by date)](https://img.shields.io/github/v/release/urtzienriquez/citeref.nvim)](https://github.com/urtzienriquez/citeref.nvim/releases) [![Tests](https://github.com/urtzienriquez/citeref.nvim/actions/workflows/test.yml/badge.svg)](https://github.com/urtzienriquez/citeref.nvim/actions/workflows/test.yml)

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

</div>

citeref.nvim lets you search your BibTeX library and insert formatted citations without leaving Neovim, and browse the named code chunks in your R Markdown or Quarto documents to insert figure and table cross-references. It works with your existing fuzzy picker and/or completion plugin — no new UI to learn.
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 in the current buffer and its sibling `.rmd`/`.qmd` files. Supported languages are **R**, **Python**, and **Julia** — all three are recognised in both R Markdown and Quarto documents. Select a chunk and citeref inserts the right reference syntax for your document type — `\@ref(fig:label)` for R Markdown, or `@label` for Quarto's native cross-reference format.
**Cross-references** are generated by scanning the named code chunks and LaTeX `\label{}` definitions in the current buffer and its sibling `.rmd`/`.qmd`/`.tex` files. For code chunks, supported languages are **R**, **Python**, and **Julia** — all three are recognised in both R Markdown and Quarto documents. 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.

Contributions are very welcomed! See [Contributing](#contributing)

Expand Down Expand Up @@ -107,6 +107,7 @@ When using a completion backend (`blink` or `cmp`), citeref activates automatica
| `@` | 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)` |
Expand Down Expand Up @@ -313,14 +314,20 @@ require("citeref").setup({

## Cross-references

The crossref pickers scan code chunks in:
The crossref pickers scan:

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

### Supported languages
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.

citeref recognises 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.
### 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.

**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

Expand Down Expand Up @@ -358,14 +365,43 @@ 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.

### LaTeX label syntax

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

```latex
\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 | Figure crossref | Table crossref |
| ----------------- | -------------------- | -------------------- |
| `quarto` (`.qmd`) | `@fig-example` | `@tbl-example` |
| `rmd`, `markdown` | `\@ref(fig:example)` | `\@ref(tab:example)` |
| 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) |
| `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) 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.

Expand Down
19 changes: 16 additions & 3 deletions lua/citeref/backends/blink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ local function citation_items(format)
end

local function crossref_items(ref_type)
local chunks = parse.load_chunks()
local chunks = parse.load_labels(ref_type)
local bufnr = vim.api.nvim_get_current_buf()
local items = {}
for _, c in ipairs(chunks) do
Expand All @@ -64,7 +64,7 @@ local function crossref_items(ref_type)
detail = "⚠ needs a label to use in a cross-reference"
kind_val = KIND.Field
else
insert = parse.format_crossref(ref_type, c.label, bufnr)
insert = parse.format_crossref(ref_type, c.label, bufnr, c.source)
detail = ref_type .. " · line " .. c.line .. (c.is_current and "" or " · " .. vim.fn.fnamemodify(c.file, ":t"))
kind_val = KIND.Value
end
Expand All @@ -82,7 +82,7 @@ end
--- In Quarto, crossrefs use plain `@label` regardless of fig/tab type,
--- so each chunk should appear exactly once rather than twice.
local function crossref_items_quarto()
local chunks = parse.load_chunks()
local chunks = parse.load_labels(nil)
local items = {}
for _, c in ipairs(chunks) do
local insert, detail, kind_val
Expand Down Expand Up @@ -160,6 +160,19 @@ function Source:get_completions(ctx, callback)
return
end

-- LaTeX crossref trigger: \ref{…}
if (ft == "tex" or ft == "latex") and before:match("\\ref{[%w_%-:.]*$") then
local items = {}
vim.list_extend(items, crossref_items("fig"))
vim.list_extend(items, crossref_items("tab"))
callback({
items = items,
is_incomplete_forward = false,
is_incomplete_backward = false,
})
return
end

-- R Markdown crossref trigger: fires from \@ onward, covering \@, \@ref, \@ref(, \@ref(partial.
-- Must be checked before the plain @ trigger to avoid the @ in \@ matching citations.
if ft ~= "quarto" and before:match("\\@%a*%(?[%w_%-%.]*$") then
Expand Down
15 changes: 12 additions & 3 deletions lua/citeref/backends/cmp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ local function citation_items(format)
end

local function crossref_items(ref_type)
local chunks = parse.load_chunks()
local chunks = parse.load_labels(ref_type)
local bufnr = vim.api.nvim_get_current_buf()
local items = {}
for _, c in ipairs(chunks) do
Expand All @@ -64,7 +64,7 @@ local function crossref_items(ref_type)
detail = "⚠ needs a label to use in a cross-reference"
kind_val = KIND.Field
else
insert = parse.format_crossref(ref_type, c.label, bufnr)
insert = parse.format_crossref(ref_type, c.label, bufnr, c.source)
detail = ref_type .. " · line " .. c.line .. (c.is_current and "" or " · " .. vim.fn.fnamemodify(c.file, ":t"))
kind_val = KIND.Value
end
Expand All @@ -82,7 +82,7 @@ end
--- In Quarto, crossrefs use plain `@label` regardless of fig/tab type,
--- so each chunk should appear exactly once rather than twice.
local function crossref_items_quarto()
local chunks = parse.load_chunks()
local chunks = parse.load_labels(nil)
local items = {}
for _, c in ipairs(chunks) do
local insert, detail, kind_val
Expand Down Expand Up @@ -164,6 +164,15 @@ function Source:complete(request, callback)
return
end

-- LaTeX crossref trigger: \ref{…}
if (ft == "tex" or ft == "latex") and before:match("\\ref{[%w_%-:.]*$") then
local items = {}
vim.list_extend(items, crossref_items("fig"))
vim.list_extend(items, crossref_items("tab"))
callback({ items = items, isIncomplete = false })
return
end

-- R Markdown crossref trigger: fires from \@ onward, covering \@, \@ref, \@ref(, \@ref(partial.
-- Must be checked before the plain @ trigger to avoid the @ in \@ matching citations.
if ft ~= "quarto" and before:match("\\@%a*%(?[%w_%-%.]*$") then
Expand Down
2 changes: 1 addition & 1 deletion lua/citeref/backends/fzf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ function M.pick_crossref(ref_type, chunks, ctx)
end, 100)
return
end
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr)
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr, chunk.source)
util.insert_at_context(ctx, crossref)
vim.defer_fn(function()
vim.notify("citeref: inserted " .. crossref, vim.log.levels.INFO)
Expand Down
2 changes: 1 addition & 1 deletion lua/citeref/backends/minipick.lua
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ function M.pick_crossref(ref_type, chunks, ctx)
end)
return
end
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr)
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr, chunk.source)
insert_after_pick(ctx, crossref)
end,
},
Expand Down
2 changes: 1 addition & 1 deletion lua/citeref/backends/snacks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ function M.pick_crossref(ref_type, chunks, ctx)
vim.notify("citeref: chunk has no label – add a label to use it in a cross-reference", vim.log.levels.WARN)
return
end
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr)
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr, chunk.source)
util.insert_at_context(ctx, crossref)
vim.notify("citeref: inserted " .. crossref, vim.log.levels.INFO)
end)
Expand Down
2 changes: 1 addition & 1 deletion lua/citeref/backends/telescope.lua
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ function M.pick_crossref(ref_type, chunks, ctx)
end, 100)
return
end
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr)
local crossref = parse.format_crossref(ref_type, chunk.label, ctx.bufnr, chunk.source)
util.insert_at_context(ctx, crossref)
vim.defer_fn(function()
vim.notify("citeref: inserted " .. crossref, vim.log.levels.INFO)
Expand Down
16 changes: 8 additions & 8 deletions lua/citeref/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,16 @@ end

function M.crossref_figure()
if is_picker() then
local chunks = parse.load_chunks()
if #chunks == 0 then
vim.notify("citeref: no code chunks found.", vim.log.levels.WARN)
local labels = parse.load_labels("fig")
if #labels == 0 then
vim.notify("citeref: no figure labels found.", vim.log.levels.WARN)
return
end
-- Capture filetype now (before picker opens and focus changes)
local bufnr = vim.api.nvim_get_current_buf()
local ctx = require("citeref.util").save_context()
ctx.bufnr = bufnr
registry.call("pick_crossref", "fig", chunks, ctx)
registry.call("pick_crossref", "fig", labels, ctx)
elseif insert_mode() then
registry.call("show", "crossref_fig")
else
Expand All @@ -166,15 +166,15 @@ end

function M.crossref_table()
if is_picker() then
local chunks = parse.load_chunks()
if #chunks == 0 then
vim.notify("citeref: no code chunks found.", vim.log.levels.WARN)
local labels = parse.load_labels("tab")
if #labels == 0 then
vim.notify("citeref: no table labels found.", vim.log.levels.WARN)
return
end
local bufnr = vim.api.nvim_get_current_buf()
local ctx = require("citeref.util").save_context()
ctx.bufnr = bufnr
registry.call("pick_crossref", "tab", chunks, ctx)
registry.call("pick_crossref", "tab", labels, ctx)
elseif insert_mode() then
registry.call("show", "crossref_tab")
else
Expand Down
Loading
Loading