Editor integration

mdwright ships a built-in language server. Point your editor at mdwright lsp and you get diagnostics, hover docs, quick-fixes, and on-save / on-type formatting without configuring an external formatter command.

The smallest possible config is Helix:

[language-server.mdwright]
command = "mdwright"
args = ["lsp"]

[[language]]
name = "markdown"
language-servers = ["mdwright"]

Position encoding gotcha

mdwright advertises UTF-8 position encoding. Clients that negotiate UTF-8 (VS Code 1.74+, Helix, Zed, neovim 0.10+) get the full surface: diagnostics, hover, formatting, range formatting, on-type formatting, and code actions. Clients that only support UTF-16 get diagnostics + hover; formatting and code-action providers are withdrawn rather than risk corrupting non-ASCII sources, and a warning is logged via window/logMessage. Check your editor's LSP log if formatting unexpectedly does nothing, it usually means the client never granted UTF-8.

VS Code

mdwright does not publish a dedicated VS Code extension. Install a generic LSP-client extension that lets you point at an arbitrary LSP binary, then configure it to launch mdwright lsp:

{
  "[markdown]": {
    "editor.defaultFormatter": "<your-lsp-client-extension-id>"
  },
  "yourLspClient.servers": [
    {
      "command": "mdwright",
      "args": ["lsp"],
      "languages": ["markdown"]
    }
  ]
}

Helix

Add to ~/.config/helix/languages.toml (or the workspace .helix/languages.toml):

[language-server.mdwright]
command = "mdwright"
args = ["lsp"]

[[language]]
name = "markdown"
language-servers = ["mdwright"]
auto-format = true

:lsp-restart after editing. Helix's space-a opens the code-action menu; pick Fix `bare-url`: … for a single diagnostic or Apply all mdwright safe fixes to run every safe quick-fix at once.

Zed

Add to ~/.config/zed/settings.json:

{
  "lsp": {
    "mdwright": {
      "binary": {
        "path": "mdwright",
        "arguments": ["lsp"]
      }
    }
  },
  "languages": {
    "Markdown": {
      "language_servers": ["mdwright"],
      "format_on_save": "on"
    }
  }
}

Neovim

Using nvim-lspconfig on neovim 0.10+:

local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")

if not configs.mdwright then
  configs.mdwright = {
    default_config = {
      cmd = { "mdwright", "lsp" },
      filetypes = { "markdown" },
      root_dir = lspconfig.util.find_git_ancestor,
      settings = {},
    },
  }
end

lspconfig.mdwright.setup({
  on_attach = function(_, bufnr)
    vim.api.nvim_create_autocmd("BufWritePre", {
      buffer = bufnr,
      callback = function() vim.lsp.buf.format({ async = false }) end,
    })
  end,
})

Configuration

The server discovers .mdwright.toml, mdwright.toml, or pyproject.toml's [tool.mdwright] table by walking up from the workspace root, exactly like the CLI. Edit one of those files and the server re-lints every open buffer on the next file-watcher event; workspace/didChangeConfiguration triggers the same refresh.

The LSP server keeps the same default input-size boundary as the CLI: a single open buffer above 10 MB stays open, but mdwright publishes one document-level diagnostic and suppresses linting, formatting, range formatting, and code actions for that version.

Range-formatting caveats

textDocument/rangeFormatting and textDocument/onTypeFormatting snap the requested range out to the nearest whole top-level block before formatting. For sources without document-scope reorderable constructs the snapped output is a verbatim substring of the whole-document format; link definitions ([label]: dest) and footnote definitions ([^label]: …) are document-scope, so a range format may leave them in place where a whole-document format would have moved them to the canonical location. Save the file (or invoke whole-document formatting) periodically to reconcile.

Smoke test

Before publishing an editor integration, run this manual check:

  1. Start the server with mdwright lsp.
  2. Open a Markdown file that contains https://example.com and confirm the bare-url diagnostic appears.
  3. Insert - [n]:Z followed by a carriage return, newline, and two tabs. The server should publish one parser diagnostic at the start of the file and keep running.
  4. Replace the file contents with valid Markdown. Normal diagnostics should return without restarting the server.
  5. Run whole-document formatting and range formatting on a paragraph that mdwright changes.
  6. Edit .mdwright.toml and trigger your editor's LSP config reload or file-watcher refresh. Open buffers should be re-linted with the new policy.
  7. Check the editor's LSP log if formatting is unavailable; the common cause is a client that did not negotiate UTF-8 positions.

See also