Language server protocol (LSP)¶
rumdl includes a built-in LSP server for real-time Markdown linting in your editor.
Starting the server¶
# Default: use stdio (for editor integration)
rumdl server
# With custom config
rumdl server --config .rumdl.toml
# Verbose logging (for debugging)
rumdl server --verbose
# TCP mode (for debugging)
rumdl server --port 9257
Capabilities¶
The rumdl LSP server provides:
- Diagnostics: Real-time linting as you type
- Code actions: Quick fixes for auto-fixable issues
- Document formatting: Format entire document (
rumdl fmt) - Range formatting: Format selected text
- Completion: Language suggestions for fenced code blocks, plus file paths and heading anchors inside link targets
- Link navigation: Hover preview, go-to-definition, find-references, and rename for Markdown links
Code block language completion¶
When typing a fenced code block, rumdl provides intelligent completions for language labels.
Type ```py and completions will appear for languages starting with "py" (Python, etc.).
The completion uses GitHub Linguist data (799+ languages) and respects your MD040 configuration:
[MD040]
# Only suggest these languages
allowed-languages = ["Python", "JavaScript", "Rust"]
# Or exclude specific languages
disallowed-languages = ["HTML"]
# Prefer specific aliases
preferred-aliases = { Python = "py", JavaScript = "js" }
Features:
- Triggers after
```or~~~fence markers - Supports extended fences (4+ backticks for nested blocks)
- Filters by
allowed-languagesanddisallowed-languages - Prioritizes
preferred-aliasesin results - Shows canonical language name in completion details
Link path and anchor completion¶
Inside a Markdown link target, rumdl suggests workspace file paths after ](
and heading anchors after # (for example ](../guide.md# lists the headings
in guide.md). This is driven by a workspace index of Markdown files and their
headings.
If you use another language server for link completion (for example a PKM/notes
LSP) and do not want rumdl's suggestions, disable it with the
enableLinkCompletions setting (see LSP settings). When
disabled, rumdl returns no link suggestions and does not register the
link-target trigger characters ((, #, /, ., -), so it is not invoked on
them; fenced code-block language completion still works. Linting, formatting,
and code actions are unaffected. The related navigation features (hover,
go-to-definition, references, rename) are controlled separately by
enableLinkNavigation.
Editor configuration¶
Neovim (nvim-lspconfig)¶
Add to your Neovim configuration:
-- if you do not use nvim-lspconfig, add this rumdl config
vim.lsp.config("rumdl", {
cmd = { "rumdl", "server" },
filetypes = { "markdown" },
root_markers = { ".git", ".rumdl.toml" },
settings = {
rumdl = {
lineLength = 100,
},
},
})
vim.lsp.enable("rumdl")
Helix¶
Add to languages.toml:
[language-server.rumdl]
command = "rumdl"
args = ["server"]
[[language]]
name = "markdown"
language-servers = ["rumdl"]
formatter = { command = "rumdl", args = ["check", "--fix", "--stdin"] }
Note: The
[[language]]block replaces the Helix defaults. Add any other language servers you use (e.g.,marksman) to thelanguage-serverslist. rumdl was merged into Helix's built-in config after the 25.07.1 release, so manual configuration will not be needed once the next Helix version ships.
VS Code¶
Install the rumdl VS Code extension from the marketplace.
The extension automatically manages the LSP server.
Zed¶
Add to your Zed settings:
{
"lsp": {
"rumdl": {
"binary": {
"path": "rumdl",
"arguments": ["server"]
}
}
},
"languages": {
"Markdown": {
"language_servers": ["rumdl"]
}
}
}
Sublime Text¶
For information on configuring Sublime Text with rumdl as a language server, see the LSP for Sublime Text documentation.
Emacs (lsp-mode)¶
Add to your Emacs configuration:
(with-eval-after-load 'lsp-mode
(add-to-list 'lsp-language-id-configuration '(markdown-mode . "markdown"))
(lsp-register-client
(make-lsp-client
:new-connection (lsp-stdio-connection '("rumdl" "server"))
:major-modes '(markdown-mode)
:server-id 'rumdl)))
Emacs (eglot)¶
Add to your Emacs configuration:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(markdown-mode . ("rumdl" "server"))))
Configuration¶
The LSP server uses the same configuration as the CLI. It automatically discovers .rumdl.toml or pyproject.toml in your project.
You can override the config path:
Or use built-in defaults only:
LSP settings¶
Beyond the config file, editors can pass settings to the server as LSP
initialization options (or workspace/didChangeConfiguration). These are
top-level keys in camelCase, following Ruff's LSP convention:
| Setting | Default | Description |
|---|---|---|
enableLinting |
true |
Real-time diagnostics as you type |
enableAutoFix |
false |
Apply auto-fixes on save |
enableLinkCompletions |
true |
File-path and heading-anchor completions inside link targets. Set to false to keep linting while letting another LSP own link completion. |
enableLinkNavigation |
true |
Hover, go-to-definition, find-references, and rename for links. Set to false to avoid conflicts with another LSP that provides these. |
enableSymbols |
true |
Document outline (documentSymbol) and workspace heading search (workspace/symbol). Set to false to avoid duplicate headings when another LSP provides the outline. |
linkCompletionContentRoots |
[] |
Roots for absolute-style link completion (e.g. /img/01.webp); defaults to the workspace roots. |
configPath |
(auto) | Explicit path to a rumdl config file |
disableRules / enableRules |
(config) | Override which rules run |
settings |
(config) | Rule overrides. lineLength sets the global line length; a rule key such as MD013 sets per-rule options (e.g. settings = { lineLength = 100 } or settings = { MD013 = { lineLength = 120 } }) |
The same options reach the server two ways, with slightly different nesting:
- Initialization options (
init_optionsinvim.lsp.config): the top-level keys above are passed directly, and rule overrides go undersettings, e.g.init_options = { settings = { lineLength = 100 } }. - Workspace configuration (
settingsinvim.lsp.config, sent viaworkspace/didChangeConfiguration): everything is nested under arumdlsection, with rule overrides directly under it, e.g.settings = { rumdl = { lineLength = 100 } }.
Both are honored; use whichever your client makes easier.
For example, to run rumdl alongside a navigation-focused Markdown LSP (such as marksman or markdown-oxide) as a pure linter/formatter - keeping its diagnostics, fixes, and formatting while letting the other server own completion, navigation, and the heading outline - in Neovim:
vim.lsp.config("rumdl", {
cmd = { "rumdl", "server" },
filetypes = { "markdown" },
root_markers = { ".git", ".rumdl.toml" },
init_options = {
enableLinkCompletions = false,
enableLinkNavigation = false,
enableSymbols = false,
},
})
These keys are read from the server's initialization options, so any editor that
can pass initializationOptions to a language server can set them. Capability
flags like enableSymbols are negotiated when the server starts, so changes take
effect after the server (re)starts.
Troubleshooting¶
Enable verbose logging¶
This outputs detailed logs to stderr, which most editors capture in their LSP logs.
Check server is working¶
Test the server manually:
You should see a JSON response with server capabilities.
Common issues¶
Diagnostics not appearing:
- Ensure the file is recognized as Markdown (check file extension)
- Check that rumdl is in your PATH
- Look at your editor's LSP logs for errors
Wrong config being used:
- Use
--verboseto see which config file is loaded - Use
--configto specify an explicit path - Use
--no-configto ignore all config files