Skip to content

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-languages and disallowed-languages
  • Prioritizes preferred-aliases in results
  • Shows canonical language name in completion details

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 the language-servers list. 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:

rumdl server --config /path/to/.rumdl.toml

Or use built-in defaults only:

rumdl server --no-config

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_options in vim.lsp.config): the top-level keys above are passed directly, and rule overrides go under settings, e.g. init_options = { settings = { lineLength = 100 } }.
  • Workspace configuration (settings in vim.lsp.config, sent via workspace/didChangeConfiguration): everything is nested under a rumdl section, 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

rumdl server --verbose

This outputs detailed logs to stderr, which most editors capture in their LSP logs.

Check server is working

Test the server manually:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}' | rumdl server

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 --verbose to see which config file is loaded
  • Use --config to specify an explicit path
  • Use --no-config to ignore all config files

See also