6 releases

new 0.0.5 May 1, 2025
0.0.4 Feb 18, 2025
0.0.3 Nov 7, 2024
0.0.2 Jul 28, 2024
0.0.0 May 31, 2024

#1074 in Text processing

Download history 1/week @ 2025-02-08 106/week @ 2025-02-15 26/week @ 2025-02-22 5/week @ 2025-03-01 2/week @ 2025-04-05 54/week @ 2025-04-26

56 downloads per month

MIT license

96KB
2K SLoC

mdBook Language Server

mdBook-LS provides a language server to preview mdBook projects live, patching the edited chapter instantly and asynchronously as you type in your editor.

mdBook-LS Features

https://github.com/SichangHe/mdbook_ls/assets/84777573/f75eb653-a143-4191-9c87-e6cb6064e6bc

  • Live preview: Instantly see the latest preview as you type in the editor.
  • Asynchronous patching: No blocking your editor; under high load, always tries to render the latest version while showing intermediate feedbacks, using a two-JoinSet.
  • Peripheral watching: Change the important files of your project (.gitignore, book.toml, SUMMARY.md, and the theme directory) and see the book fully rebuilt; it reloads the file watcher and the web server as needed.
  • Refresh a patched page to manually trigger a full rebuild.

Editor Setup

Installation with, e.g., Cargo.
cargo install mdbook_ls

✅ NeoVim setup with LSPConfig

Please paste the below register_mdbook_ls function in your Nvim configuration, call it, and then set up mdbook_ls like any other LSPConfig language server. Please see my config for an example.

The snippet provides two Vim commands: MDBookLSOpenPreview starts the preview (if not already started) and opens the browser at the chapter you are editing; MDBookLSStopPreview stops updating the preview (Warp may keep serving on the port despite being cancelled).

The mdbook_ls_setup function.
local function register_mdbook_ls()
    local lspconfig = require('lspconfig')
    local function execute_command_with_params(params)
        local clients = lspconfig.util.get_lsp_clients {
            bufnr = vim.api.nvim_get_current_buf(),
            name = 'mdbook_ls',
        }
        for _, client in ipairs(clients) do
            client.request('workspace/executeCommand', params, nil, 0)
        end
    end
    local function open_preview()
        local params = {
            command = 'open_preview',
            arguments = { "127.0.0.1:33000", vim.api.nvim_buf_get_name(0) },
        }
        execute_command_with_params(params)
    end
    local function stop_preview()
        local params = {
            command = 'stop_preview',
            arguments = {},
        }
        execute_command_with_params(params)
    end

    require('lspconfig.configs').mdbook_ls = {
        default_config = {
            cmd = { 'mdbook-ls' },
            filetypes = { 'markdown' },
            root_dir = lspconfig.util.root_pattern('book.toml'),
        },
        commands = {
            MDBookLSOpenPreview = {
                open_preview,
                description = 'Open mdBook-LS preview',
            },
            MDBookLSStopPreview = {
                stop_preview,
                description = 'Stop mdBook-LS preview',
            },
        },
        docs = {
            description = [[The mdBook Language Server for previewing mdBook projects live.]],
        },
    }
end

I plan to merge this into nvim-lspconfig in the future.

❓ Visual Studio Code and other editor setup

No official support, but community plugins are welcome.

I do not currently use VSCode and these other editors, so I do not wish to maintain plugins for them.

However, it should be straightforward to implement plugins for them since mdBook-LS implements the Language Server Protocol (LSP). So, please feel free to make a plugin yourself and create an issue for me to link it here.

mdBook Incremental Preview

mdBook-Incremental-Preview powers the live preview feature of mdBook-LS. It can also be used standalone if you only wish to update the preview on file saves.

mdBook-Incremental-Preview provides incremental preview building for mdBook projects. Unlike mdbook watch or mdbook serve, which are inefficient because they rebuild the whole book on file changes, mdBook-incremental-preview only patches the changed chapters, thus producing instant updates.

Usage of mdBook Incremental Preview

At your project root, run:

mdbook-incremental-preview

It has basically the same functionality as mdbook serve but incremental:

  • Chapter changes are patched individually and pushed to the browser, without refresh.
  • Full rebuilds happen only when the .gitignore, book.toml, SUMMARY.md, or the theme directory changes, or a patched page is requested by a new client.
  • Build artifacts are stored in a temporary directory in memory.
  • It directly serves static files, additional JS & CSS, and asset files from the source directory, instead of copying them.

Details of patching

When a chapter changes, we push its patched content to the corresponding browser tabs and replace the contents of their <main> elements. So, the browser does not reload the page, but updates the content instantly.

After replacing the content, our injected script issues a load window event. You should listen to this event to rerun any JavaScript code as needed. An example is below in the MathJax support section.

Current limitations of patching

  • Preprocessors that operate across multiple book item are not supported. The results may be incorrect, or the implementation may fall back to a full rebuild. This is because we feed the preprocessors the individual chapters rather than the whole book when patching.

    This is irrelevant for most preprocessors, which operate on a single chapter. Even the link preprocessor works because it reads the input files directly.

  • Neither print.html or the search index are updated incrementally. They are only rebuilt on full rebuilds, which can be triggered by refreshing a patched page.

  • The book template (index.hbs) has to include exactly {{ content }} in the <main> tag (the default), otherwise the patching will not work correctly. A workaround would be to allow custom injected scripts, but I will not implement that unless demanded.

  • Search support has been disabled to reduce complexity and overhead. It is not that useful for live preview anyway.

MathJax support

MathJax.js is too slow for live preview, so you should instead consider mdBook-KaTeX, client-side KaTeX (with a custom script that listens to the load event, as mentioned above), or other alternatives.

If you have to stick with MathJax, please add a custom script that listens to the load event and reruns MathJax, like this:

document.addEventListener("load", () => MathJax.Hub.Typeset());

Debugging

We use tracing-subscriber with the env-filter feature to emit logs^tracing-env-filter. Please configure the log level by setting the RUST_LOG environment variable.

Contributing

I welcome high-quality issues and pull requests.

Future work

  • Unit tests so I do not need to test it in the editor on every commit.
  • Integrate with Open Telemetry so I do not need to stare at all the logs.

Dependencies

~29–44MB
~709K SLoC