logoxide-lsp 0.1.1

A language server for Bible references and text: completion, hover, symbols, diagnostics, and passages from logoxide packages
# logoxide language server

`logoxide-lsp` speaks the Language Server Protocol over stdin and stdout. It was topos's `topos-lsp`
until October 2026; it moved here so its Bible text can come from logoxide packages, while topos
stays references only:

- **Completion**: book names (`1 Co` → `1 Corinthians`), then chapters and verses from the
  book's versification; each completion rewrites the whole reference (`jn 3:1` → `John 3:16`).
  With a `bible`, chapters and verses resolve (`completionItem/resolve`) to their text
- **Hover**: the reference under the cursor, normalized, with what the `hover` setting lists:
  the passage's text (with a `bible`), its full name and abbreviation, OSIS id, book (and its
  number and chapters), testament, genres, verses, and where it is. On a reference that doesn't
  exist, what does (`John 3 has 36 verses`, `John has 21 chapters`). With `commentaries`, the
  project's commentaries' notes on the passage follow its text; with `strongs`, hovering a
  Strong's number (`G25`, `H430`) shows its dictionary entry (see
  [Commentaries and Strong's numbers]#commentaries-and-strongs-numbers)
- **Go to definition** and **declaration** (with a `bible`): the passage in its whole book, a read-only document
  like `logoxide://BSB/JHN` (see [Whole books]#whole-books)
- **Inlay hints**: after each reference, how it's written in your format (where that differs,
  like `jn 3:16` → `John 3:16`), or its OSIS id
- **Document symbols**: every reference in the file, for outlines and pickers
- **Go to references**: every reference in the workspace that is exactly the one under the
  cursor (`--exact-overlap`), however it's written. In a whole book, every reference that names
  the verse under the cursor (`--explicit-overlap`), or on a chapter's label, every reference
  that shares a verse with the chapter
- **Code actions**: on a reference, reformat it the way completion writes it (`Reformat as
  "John 3:16"`, and `Reformat all N references in this file` when several differ), or search the
  workspace for it: `Search "John 3:16" for explicit overlap`, `... for any overlap`,
  `... for exact overlap`, and `Search inside "John 3"`. With a `bible`, one action per
  template with an `action` (`Insert callout`) writes the passage's text below the reference,
  or in place of its line or of the reference
- **Diagnostics**, pushed, or pulled when the editor asks for them (`textDocument/diagnostic`, for
  editors that support it): an information diagnostic on each reference with how it's written in your
  format (`John 3:16`, code `reference`), and warnings for references that do not exist, like
  `John 3:99` (code `missing`)

Workspace searches cover every text file in the workspace folders (and `search-folders`), skipping hidden, binary, and
ignored files (`.gitignore`, `.ignore`, `.toposignore`), like the CLI. Open files are searched
as they are in the editor, including unsaved changes. With `epub` (and `pdf`), books are searched
too: each is read once, then its references come from topos's cache (the CLI's), and a result
opens the book's text at the reference (logoxide.nvim opens `logoxide-file://` documents).

Install with:

```sh
cargo install --path crates/logoxide-lsp     # in a logoxide checkout, next to a topos checkout
cargo install --path crates/logoxide-lsp --features pdf   # for the `pdf` setting (builds MuPDF)
```

## Settings

The server reads the CLI's `~/.config/topos/config.toml` (or `$XDG_CONFIG_HOME/topos/config.toml`),
so one file configures both. It uses these keys and ignores the others:

| Key | Effect |
|---|---|
| `format`, `psg-fmt`, `fmt-*` | How references are written in completions, hover, hints, symbols, and action titles (see the CLI's "Writing references") |
| `data`, `merge-data`, `remove-data` | Custom book data |
| `context-book`, `context-heading` | References without a book (`3:16`) |
| `ext` | Extensions searched in the workspace, like `"md,txt"` (all text files by default) |
| `search-folders` | More folders that go-to-references and the search command cover, beside the workspace folders: `"~/Obsidian"` or a list (relative ones are in the first workspace folder). They are only searched: a `logoxide.toml` in one doesn't make it the project |
| `epub` | Search EPUBs too, whatever `ext` says (`false` by default). Their references are kept in topos's cache, shared with the CLI, and are in `logoxide-file:///…/Gill.epub` documents: the book's text, from the `logoxide/fileText` request (`{ uri }`, giving `{ title, text }`) |
| `pdf` | Search PDFs the same way (`false` by default; only in a build with `--features pdf`) |
| `inlay-hints` | `"changed"` (default), `"always"`, `"osis"`, `"usfm"`, `"never"`, a template's name in `templates` (with its markup), or a template with their placeholders (`"{first:1}"`, its first verse's text), shown after every reference on one line |
| `reference-diagnostics` | The diagnostic on each reference: `"info"` (default), `"hint"`, or `"never"` |
| `reference-diagnostics-template` | Their text, on one line: a template's name in `templates` (with its markup), or a template (`"{first:1}"`); the reference as formatted by default, and when the Bible lacks the passage |
| `complete-typed` | Completion offers the chapter or verse already typed, first (`Matthew 1` offers `Matthew 1`, then `Matthew 10`, ...), so its passage shows; `true` by default. The CLI's `--complete` uses it too |
| `hover` | What a hover shows below the reference, in order (a list or `"osis,book"`; `[]` for none): `text` (the passage, from `bible`), `written`, `name`, `abbreviation`, `osis`, `book`, `testament`, `genres`, `verses`, `bcv`, `location`, `translation` (the Bible the reference names, if it names one). The default is every one but `written`, `bcv`, and `translation`. `name` and `abbreviation` are left out when the reference is already written that way |
| `hover-trim` | Hover and completion documentation leave out the leading whitespace of a passage's text (a note's, an entry's), like the blank line a heading displayed as `"\n{text}"` starts with; `false` by default |
| `hover-line` | Hovering a line away from any reference shows every reference on it, one after another between rules, so a line's only reference hovers from anywhere on the line; hovering a reference still shows just it. `false` by default |
| `hover-title` | The hover's first line, a template: `{reference}` (as formatted), `{bible}` (the Bible whose text is shown first), `{translation}` (the Bible the reference names; when it names none, nothing, with the brackets around it and the space before it); `""` for no first line. Default `"**{reference}** ({translation})"` |
| `book-hover` | What hovering a verse in a whole book shows, in order: `references` (how many references name it, in how many files), `translations` (the verse in the other Bibles, each labeled: `hover-bibles`' when set, else every other), and `current` (the verse in the book's own Bible, labeled the same way); a chapter's label has no texts. `"current,translations,references"` or a list; `[]` for no hover. Default `["references"]` |
| `hover-max` | How many bytes of a passage's text (a note's, an entry's) hover and completion documentation show before cutting it with ` …`: `2000` by default, `0` for no limit |
| `bible` | A Bible's folder: a logoxide package (with a `logoxide.toml`; USX or USFM, named by its abbreviation), or a folder of USX files, one book each (`JHN.usx`, `44JHNBSB.usx`, or any name with a `<book code>` inside), as Paratext, the Digital Bible Library, and eBible.org publish them, named after the folder. Its files are left out of workspace searches |
| `bibles` | More Bibles: package ids from the project's `logoxide.lock` (`["bsb", "kjv"]`), or folders by name: `{ KJV = "~/Bibles/KJV" }` or `{ KJV = { path = "~/Bibles/KJV", aliases = ["AV"] } }` (see [Translations]#translations). Without `bible` or `bibles`, the project's Bibles are used (not those turned off with `lx disable`) |
| `project` | The folder whose `logoxide.toml` and `logoxide.lock` name the Bibles: by default a workspace folder that has one (a vault), else the library's own (`lx add` without a project) |
| `library` | The logoxide library the packages are installed in (`LOGOXIDE_HOME`, else `$XDG_DATA_HOME/logoxide`) |
| `default-bible` | The Bible for references that name none (a name or alias): `bible`'s by default, else the first of `bibles` by name |
| `hover-bibles` | Bibles whose text hover also shows, after the one the reference names (`["BSB", "KJV"]`); one numbered differently shows the passage as it numbers it when the project has a versification map package between the two |
| `commentaries` | Commentaries whose notes on the passage hover shows after its text: `true` for every commentary the project uses (`kind = "commentary"`, not those turned off), or their ids (`["mhcc"]`); none by default |
| `strongs` | Hovering a Strong's number shows its entry in the project's Strong's dictionaries (`kind = "lexicon"` with `keys = "strongs-greek"` or `"strongs-hebrew"`); `false` by default |
| `markup` | How Bible text is written (see below) |
| `templates` | Named templates: code actions that write a passage's text, forms to copy it in, and `inlay-hints` (see below) |

The CLI skips `inlay-hints`, `reference-diagnostics`, `reference-diagnostics-template`, `hover`, `bible`, `bibles`, `project`,
`library`, `default-bible`, `hover-bibles`, `markup`, and `templates`, so they can go in the shared `config.toml`.

`markup` has a table for each kind of element, with `show` and `display` (the defaults are
shown; other keys, like an editor's `color`, are ignored):

```toml
[markup]
verse_numbers = { show = true, display = "[{verse}]" }
chapter_numbers = { show = true, display = "{name} {chapter}" } # in whole books, on their own line
headings = { show = true, display = "{text}" }
notes = { show = false, display = "[{text}]" } # footnotes
words_of_jesus = { show = true, display = "{text}" }
added = { show = true, display = "{text}" }        # translators' additions (KJV italics)
divine_name = { show = true, display = "{text}" }  # LORD in small capitals
quotes = { show = true, display = "{text}" }       # quotations other than words of Jesus
width = 80 # break lines longer than this at a space (none by default)
indent = { q1 = 0, q2 = 2, q3 = 4, q4 = 6 } # spaces before each line of poetry, by USFM marker
verse_lines = false    # each verse on a line of its own
paragraph_gaps = false # a blank line between paragraphs
```

Labels (verse and chapter numbers) can use `{verse}` (`16`, or `4-5` for a bridge),
`{chapter}`, `{name}` (`John`), and `{book}` (the USX code, `JHN`). The other elements' `display`
is a template with `{text}` (and `{name}`), like `## {text}` or `“{text}”`, glued to the element's
words. Headings and notes that aren't shown are left out; other elements that aren't shown are
plain text, without their template or color.

Poetry is a line each, with a blank line between stanzas (USX's `b`). `indent` sets the spaces
each kind of line starts with, by its USFM marker: `q1` to `q4` as above unless set, and any other
`para` style you add (`qr`, `qm1`, `pi1`, `li1`; an unnumbered `q` is `q1`; OSIS's `<l level="2">`
is `q2`). The first line of a hover's passage starts at the margin.

`verse_lines` starts each verse on a new line (a verse that starts a line of poetry keeps its
indent). `paragraph_gaps` puts a blank line between paragraphs, and between a paragraph and the
poetry before or after it, but not between a heading or chapter label and its first paragraph.

`templates` are named templates for a reference. Each has a `display` (default `{text}`, the
passage; `{first:N}` and `{last:N}`, its first or last N verses; `{reference}` as formatted,
`{osis}`, `{usfm}`, `{bible}`, the Bible's name, and `{translation}`, its name unless it's the
default Bible and the reference names none, which leaves out the brackets around it and the space
before it: `{reference} ({translation})`), a `line_prefix` written before each line of the text,
its own `markup` over the one above, and an `action`, the code action that writes it: `"insert"`
(the lines below the reference: `Insert callout`), `"replace-line"` (the reference's whole line:
`Replace line with callout`), `"replace"` (just the reference, keeping the rest of its line:
`Replace with callout`), or none (no code action; for copying, or `inlay-hints`). With several
Bibles, a reference that names none offers one per Bible: `Insert callout (KJV)`.

```toml
[templates.callout]
display = "> [!bible] {reference}\n{text}"
line_prefix = "> "
markup = { headings = { show = false } }
action = "insert"

[templates.verse] # reference-diagnostics-template = "verse": the first verse, without its heading
display = "{first:1}"
markup = { headings = { show = false } }
```

The old `insertions` list is an error that says how to write it as `templates`.

The `logoxide/templates` request (`{ "textDocument": { "uri": ... }, "position": ... }`) gives each
template filled for the reference there, by name, per Bible (just the passage's text, named
`text`, when there are none): `[{ "name": "callout", "label": "callout", "bible": "BSB", "text":
..., "title": "Insert callout", "edit": { "range": ..., "newText": ... } }]` (`title` and `edit`
are `null` without an `action`), for an editor to copy a passage in a form or choose an action.

The editor's settings override the file: as initialization options, or as `settings` (changed
while the server runs, without a restart). They take the same keys, at the top level or under
`logoxide`. Two more choose the file: `config` (another path) and `no-config` (none). A `format`
can also be a whole object, like `psg-fmt`. Settings that can't be used are reported as a
warning, and the previous ones stay.

## Translations

Each Bible (`bible`, and each of `bibles`) is a translation: its name, or an alias, right after a
reference (`John 3:16 KJV`, `John 3:16, KJV`, `John 3:16 (AV)`) picks it, ignoring case, and
other references use `default-bible`. Then:

- Hover (on the reference or the name) shows that Bible's text, and the `hover-bibles` after it;
  when it shows several Bibles' texts, each is labeled with its name
- Go to definition opens its book (`logoxide://KJV/JHN`)
- Completion offers the names after a reference (`John 3:16 K` → `KJV`), whose documentation is
  that Bible's text
- The reference is checked against that Bible's own verse counts, read from its USX markers when
  it loads (with an ESV, `3 John 1:15 ESV` exists, though the default counts, the KJV's, end 3 John
  at verse 14), and diagnostics name it: `3 John 1:16 does not exist in the ESV`
- Templates use it; a reference without one offers a template's action per Bible

The `logoxide/bibles` request (no parameters) lists them, the default first: `{ "bibles": [{ "name":
"BSB", "aliases": [], "books": ["GEN", ...] }] }`. The `logoxide/sameVerse` request (`{ "uri":
"logoxide://BSB/JHN", "position": { "line": 3, "character": 0 }, "bible": "KJV" }`) gives the
location of the verse at that position in another Bible's book, from its number to its end (else
its chapter's label, else the book's start), or `null` if that Bible lacks the book, for showing
two Bibles side by side.

## Whole books

With a `bible`, go to definition returns a document named like `logoxide://BSB/JHN` (the
Bible's name, then the book's USX code), which no file backs. The editor asks for its text with
the `logoxide/book` request (`{ "uri": "logoxide://BSB/JHN" }` gives `{ "title": "John", "text":
"..." }`), shows it read-only, and opens it with the server like any document. The server then
provides:

- **Semantic tokens**: `verseNumber`, `chapterNumber`, `wordsOfJesus`, `heading`, `note`,
  `added`, `divineName`, and `quote` (other documents have none), for the editor to color
- **Go to references** from a verse, as above; **hover** on a verse says how many references
  name it and in how many files (`???` if searching takes more than half a second; files read
  until then are cached)
- Hover and go to definition on the references in its headings; no diagnostics, inlay hints, or
  reformat actions

## Passage text in hover

A passage's text in hover and completion documentation is a Markdown code block whose info
string, after the language, says what to color: `line:column:length:type` for each token, with
byte columns in the block's text and the types numbered as in the semantic tokens legend:

````markdown
```logoxide 0:0:19:3 1:0:4:0 1:33:4:0
Jesus and Nicodemus
[16] For God so loved the world. [17] ...
```
````

Editors show only the language; the Neovim plugin colors the block from it. Completion
documentation is the block without the token list, since completion menus (like blink.cmp) show
the info string; the list is in the resolved item's `data.tokens` instead. With `markup.width`, a line break replaces a space, so every byte offset (verses,
tokens) is the same as without it.

## Commentaries and Strong's numbers

Commentaries and dictionaries come from the project (`lx add` them as any package; see
`doc/entries.md` for the dictionary format). With `commentaries = true` (or a list of ids),
hovering a reference shows, after the Bible text, each note the commentaries have on any of its
verses, labeled with the commentary's name and the verses the note covers:

````markdown
*MHCC* (John 3:16-18)
```logoxide
God so loved the world, ...
```
````

A note is cut like passage text (after 2000 bytes), and after eight notes from one commentary
the rest are counted (`*MHCC*: 28 more notes`). The passage is looked up as written, in the
commentary's own numbering. A verse the commentary skips has no note, unless
`stretch-notes = true` (off by default): then a note also covers the verses after it up to the
commentary's next note in the chapter, so hovering John 3:19 shows a note on 3:17-18.

With `strongs = true`, hovering a Strong's number (an uppercase `G` or `H`, one to five digits,
perhaps a letter: `G25`, `G0025`, `H430a`; standing alone, so `h1` or `AG25` isn't one) shows its
entry in each of the project's Strong's dictionaries that has it (Greek for `G`, Hebrew for `H`),
under the number in normal form and its headword (`**G25** ἀγαπάω`), each labeled with its
dictionary's name when the project has several.

Each commentary and dictionary is opened once when the settings are read and kept: a
commentary's books are parsed when first needed (and kept in logoxide's cache, as a Bible's
are), a dictionary's files are indexed when it opens and an entry's text is read when it's
looked up. The `markup` settings write notes and entries, without verse or chapter labels.

## Neovim

The [logoxide.nvim](https://github.com/Logoxide/logoxide.nvim) plugin sets up the
server, builds it, opens whole books as read-only buffers colored by `markup`, opens the code
actions' results in Telescope (or the quickfix list), and adds search commands and pickers.

Without the plugin (Neovim 0.11+):

```lua
vim.lsp.config('logoxide', {
  cmd = { 'logoxide-lsp' },
  filetypes = { 'markdown', 'text' },
  root_markers = { '.git', '.obsidian' },
  init_options = { ['psg-fmt'] = { join_adjacent = true }, ['inlay-hints'] = 'always' },
})
vim.lsp.enable('logoxide')
```

Go to references and completion then work. The code actions need a `vim.lsp.commands['logoxide.search']`
handler to show their results (see below; the plugin's is in its `lua/logoxide/init.lua`), and
go to definition needs the editor to open `logoxide://` documents (the plugin's
`lua/logoxide/book.lua`).

## Code actions in other editors

Reformat actions in an unnamed buffer (whose URI, `file://`, names no file) carry their edits as the
`logoxide.applyEdits` command (`{ "edits": [TextEdit] }`) for the editor to apply to that buffer;
in named files they are ordinary workspace edits. Completion lists are marked incomplete, so
editors ask again as you type instead of filtering a stale list.

LSP has no standard way for a server to open the editor's references list. The code actions
run the `logoxide.search` command, which returns the matching locations
(`workspace/executeCommand` with `{ "mode": "explicit-overlap" | "any-overlap" |
"exact-overlap" | "inside", "passage": "John 3:16" }`); the editor shows them.