# markdown-oxide: a language server that indexes your whole vault

> A Rust LSP built for personal knowledge management, indexing a vault rather than a file, with wikilink and tag completion, daily notes, and distribution through six package managers.

**Feel-ix-343/markdown-oxide** — PKM Markdown Language Server

- Repository: https://github.com/Feel-ix-343/markdown-oxide
- Website: https://oxide.md
- Stars: 2,319 · Forks: 139
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/feel-ix-343-markdown-oxide

## It indexes a vault, not a file

Most Markdown language servers treat a document as a document. markdown-oxide is built for what the project description calls PKM, personal knowledge management, and that distinction runs through the whole design. The repository topics say it plainly: obsidian, obsidian-md, lsp-server, rust-language-server. The README describes it as a PKM Markdown Language Server for your favorite text editor, and the documentation site is oxide.md.

The practical meaning is that completions, references and diagnostics are computed across a directory of notes, not just the buffer you have open. That is what makes wikilink completion and tag completion worth having at all: the candidate list comes from the vault. The root markers the project asks editors to look for are `.git`, `.obsidian` and `.moxide.toml`, and `.obsidian` in that list is the tell, since it is what an Obsidian vault contains. `.moxide.toml` is the project's own configuration file, documented in the Configuration Reference on oxide.md.

The feature documentation lives on the site rather than in the README, which lists only four links: what markdown-oxide is, a getting-started guide covering editor setup and PKM configuration, a Features Index, and a Configuration Reference with a default config file section. If you are deciding whether the feature set fits your notes, that Features Index is the document to read, and the README will not summarize it for you.

The credentials question does not arise here, and neither does a data store. It is a local binary that talks LSP to your editor and reads your directory. What it costs is CPU and memory during indexing, and what it gives back is a jump list that knows about notes you have not opened in months.

## Six ways to install, and the one buried in a details block

The Quick Start section is organized by editor rather than by platform, which is unusual and useful, since the install step is the same binary for all of them. Neovim, VSCode, Zed, Helix and Kakoune each get their own subsection, and the binary is distributed widely enough that most people never build it.

Package managers are listed first, with cargo install tucked into a collapsed block marked from source:

```bash
brew install markdown-oxide
```

Arch Linux has `pacman -S markdown-oxide`, Alpine has `apk add markdown-oxide`, openSUSE has `zypper install markdown-oxide`, and conda-forge carries it as `conda install conda-forge::markdown-oxide`. Nix users get `pkgs.markdown-oxide`, and Neovim users on Arch can use Mason.nvim from a hosted binary rather than compiling. Windows is covered by winget:

```bash
winget install FelixZeller.markdown-oxide
```

The from-source path is deliberately not the default, and the `--locked` flag in it is the part worth copying:

```bash
cargo install --locked --git https://github.com/Feel-ix-343/markdown-oxide.git markdown-oxide
```

There is a `cargo binstall` variant that pulls a hosted binary through the same git URL if you would rather not compile. The lockfile matters because the install pulls from the main branch rather than a tag, so without `--locked` your build can resolve different dependency versions than the one the project tested. The release profile in `Cargo.toml` is tuned accordingly, with `lto` on and `codegen-units = 1`, both of which slow a build and speed the resulting binary.

For anyone on Nix there is a `flake.nix` and a `flake.lock` at the root, and for VSCode there is a `vscode-extension/` directory in the tree, so the extension path is in the repository rather than only in a marketplace listing.

## Neovim 0.11 and the dynamicRegistration requirement

The most detailed configuration in the README is the Neovim one, and it exists because Neovim's built-in LSP support arrived in 0.11 and the config shape changed with it. If you have nvim-lspconfig installed, it ships a default `lsp/markdown_oxide.lua` with the `cmd`, `filetypes` and `root_markers` already filled in, so all you add are your own capabilities.

The README is emphatic about one detail, and it is the kind that silently breaks features. Use the function call form `vim.lsp.config('markdown_oxide', { ... })` to merge with the defaults. The assignment form `vim.lsp.config.markdown_oxide = { ... }` replaces the whole table and loses `cmd`, `root_markers` and `filetypes`. Merge, do not assign.

The capabilities themselves are not optional either:

```lua
vim.lsp.config('markdown_oxide', {
    capabilities = vim.tbl_deep_extend(
        'force',
        capabilities,
        {
            workspace = {
                didChangeWatchedFiles = {
                    dynamicRegistration = true,
                },
            },
        }
    ),
})
vim.lsp.enable('markdown_oxide')
```

`dynamicRegistration` being true is what lets the server act on file changes it registers for at runtime, and the README names two features that depend on it: the Create Unresolved File code action, and resolving completions for code blocks that have not been indexed yet. Without it the server starts fine and quietly does less.

If you are not using nvim-lspconfig, you supply the table yourself, and this is the version that shows every field the default would have set:

```lua
vim.lsp.config('markdown_oxide', {
    cmd = { 'markdown-oxide' },
    filetypes = { 'markdown' },
    root_markers = { '.git', '.obsidian', '.moxide.toml' },
    capabilities = capabilities,
})
```

For Neovim below 0.11 there is a separate `require("lspconfig").markdown_oxide.setup({...})` form. Two optional pieces finish the setup. nvim-cmp needs a keyword pattern scoped to markdown-oxide so it does not trigger on ordinary prose, and CodeLens, used for UI reference counts, needs an autocmd that refreshes codelenses on TextChanged, InsertLeave, CursorHold and BufEnter, gated on the client advertising `codeLensProvider`.

## The dependency list explains most of the feature set

`Cargo.toml` is worth reading because the choices map directly onto features you would otherwise discover one at a time. The package is `markdown-oxide` at version 0.25.12, edition 2021, and it depends on `tower-lsp` from a git URL rather than crates.io:

```toml
tower-lsp = { git = "https://github.com/Feel-ix-343/tower-lsp" }
```

That single line is the most informative thing in the manifest. Working against a fork means the project can use LSP features that have not landed upstream, which is consistent with the CodeLens and code-action support in the README. It also means the protocol surface can shift under you when the fork moves, so a pinned release is safer than tracking main.

The rest of the list reads like a feature inventory. `fuzzydate` and `chrono` handle the date parsing behind daily notes, and version 0.25.11 is where relative directives landed, which is exactly the syntax that needs fuzzy parsing. `nucleo-matcher` does fuzzy matching, `rayon` parallelizes indexing across cores, and `walkdir` is how a directory becomes a vault. `ropey` at 1.6.1 is a rope data structure for text, which is what you want for incremental work on large files rather than reallocating strings.

Two more entries explain less obvious behavior. `config` at 0.14.0 reads the `.moxide.toml` file that the root markers look for. `do-notation` is a parser-combinator crate, and it is there because Markdown is not regular: parsing wikilinks, tags and fenced blocks by hand is where the awkward cases live, and the tags-with-dashes fix in 0.25.11 came out of exactly that kind of parsing. `nanoid` and `urlencoding` are small, but they point at link handling where identifiers get generated and paths get escaped.

For binary distribution there is a `[package.metadata.binstall]` block giving a `pkg-url` template, which is how `cargo binstall` finds hosted artifacts without a registry index.

## The 10,000 line cap in 0.25.12

Release 0.25.12, published on 2026-06-29, is two pull requests and one change: indexing is capped at the first 10,000 lines of each file. The motivation is not stated, but the effect is predictable, and it is the single most important operational detail in the releases list. A note longer than that contributes its opening to the vault index and nothing after line 10,000, so wikilink and tag completions drawn from a long generated file will be incomplete, while references counted inside that region still resolve.

Version 0.25.11, ten days earlier on 2026-06-19, is a much larger release and a better map of where the project spends its effort. It added relative directive support to the daily notes command, which is the feature the `fuzzydate` dependency serves, and added a CLI for opening daily notes and config files, so some of what used to require an editor round trip can now be invoked directly. It fixed an outdated source name showing up in diagnostics, which is the sort of bug that makes a diagnostic get ignored once and then forever after.

The same release brought client-side workspace symbol filtering, contributed from outside the maintainer, which lets an editor narrow results before they cross the protocol boundary. It moved aliases onto `CompletionItemLabelDetails.description`, so an alias renders next to the note it points at rather than in the detail pane. It added Kakoune setup instructions, which fits the pattern of the project treating every editor as a first-class target. It added tag completion for tags containing dashes and underscores, which is a parser fix and also a small usability win for anyone using tags as categories. It fixed vault indexing for hidden root directories and improved LSP client compatibility, and switched path splitting to `MAIN_SEPARATOR` so the same vault works on Windows.

The cadence underneath is worth noticing. Version 0.25.10 shipped on 2025-11-02, and 0.25.11 on 2026-06-19, so roughly seven months separate them while the last push to the repository was 2026-09-28. Work continued after the last tagged release, which means the binary you install from a package manager and the code on main are not the same thing.

## What the repository is not

The tree is small and constrains what this is: `Cargo.toml`, `Cargo.lock`, `LICENSE`, `README.md`, then `src/`, `docs/`, `TestFiles/`, `vscode-extension/`, `flake.nix` and `flake.lock`. There is no web application, no sync service, no plugin system, no database and no mobile client. There are also `.agents/` and `.claude/` directories at the root, which is where the agent configuration for the project itself lives.

The `TestFiles/` directory is the interesting part to notice. For a language server, correctness is defined by how it parses what people actually write, so a directory of real notes used as fixtures says more about the project's approach than any feature list. It is also where you should look before opening an issue about a construct that does not parse.

On maturity: 2,313 stars, 134 forks and 112 open issues, which is a lot of open issues relative to the version number, and no 1.0 release. Apache-2.0 is the licence, which is the permissive choice and the reason you can read the code, take the parser ideas and ship something without a copyleft obligation. The repository is not archived and the last push was 2026-09-28.

So the honest limit is scope. If your notes are a single README or a folder of unrelated documents, a per-file language server is the better fit and the index will buy you little. If they are a linked vault with wikilinks, tags and daily notes, that index is the entire value proposition, and the 10,000 line cap is the boundary you will meet first.

## Conclusion

markdown-oxide earns its place if your notes already live in a directory with wikilinks and tags, because that is the case it indexes for and the case a per-file Markdown LSP handles poorly. The editor side is genuinely considered: five editors in the README, an nvim-lspconfig config file, a Helix and Kakoune setup, and a fork of tower-lsp for features upstream has not shipped. The trade is that the index has boundaries. Version 0.25.12 caps indexing at the first 10,000 lines of each file, so a large generated note contributes only its opening. Install with `cargo install --locked --git https://github.com/Feel-ix-343/markdown-oxide.git markdown-oxide` or `brew install markdown-oxide`, drop `.git`, `.obsidian` or `.moxide.toml` as your root marker, and turn on `didChangeWatchedFiles.dynamicRegistration` or the Create Unresolved File action never fires. The docs live at oxide.md, not in the README, which is the first place to look after setup.

## FAQ

### What is markdown-oxide?

It is a Rust language server for personal knowledge management. Rather than treating each Markdown file on its own, it indexes a whole vault so that completions for wikilinks and tags, reference counts and diagnostics are computed across all your notes.

### How do I install markdown-oxide?

Pick a package manager: `brew install markdown-oxide`, `pacman -S markdown-oxide`, `apk add markdown-oxide`, `zypper install markdown-oxide`, `conda install conda-forge::markdown-oxide`, or `winget install FelixZeller.markdown-oxide` on Windows. From source, use `cargo install --locked --git https://github.com/Feel-ix-343/markdown-oxide.git markdown-oxide`, and keep the `--locked` flag.

### Which editors does markdown-oxide support?

The README has a setup section for Neovim, VSCode, Zed, Helix and Kakoune. Neovim 0.11 and above use the built-in `vim.lsp.config` form, and nvim-lspconfig ships a default config for it. The repository also contains a `vscode-extension/` directory and a `flake.nix`.

### Why is didChangeWatchedFiles dynamicRegistration important?

The README says the server relies on it so it can act on file changes it registers at runtime. Two named features depend on it: the Create Unresolved File code action, and resolving completions for code blocks that have not been indexed yet. Without it the server starts but does less than you expect.

### How do I tell markdown-oxide where my vault starts?

It looks for root markers, and the documented set is `.git`, `.obsidian` and `.moxide.toml`. In practice you put one of those in the vault directory, or point the editor config at it. `.moxide.toml` is also the project's own configuration file, documented in the Configuration Reference on oxide.md.

### Does markdown-oxide index my entire notes file?

Not since version 0.25.12, which caps indexing at the first 10,000 lines of each file. Content past that point does not contribute to wikilink or tag completion candidates. Notes in the indexed region still resolve for references, so the effect is partial coverage on very large files rather than a broken vault.

## Sources

- [Feel-ix-343/markdown-oxide on GitHub](https://github.com/Feel-ix-343/markdown-oxide)
- [License: Apache-2.0](https://github.com/Feel-ix-343/markdown-oxide/blob/main/LICENSE)
- [Project website](https://oxide.md)
- [README](https://github.com/Feel-ix-343/markdown-oxide/blob/main/README.md)
- [Releases](https://github.com/Feel-ix-343/markdown-oxide/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/feel-ix-343-markdown-oxide
