Loom in Neovim and Helix
Neovim and Helix need no extension of the kind VS Code does. Each has a language-server client and a tree-sitter host built in, and each learns a new language from configuration. Three facts make a loom alive in either: the .loom extension names the loom language, the loom lsp --stdio command starts its language server, and the grammar of this part colours it. This chapter writes that configuration down, in the form each editor's own project collects for the languages it supports.
The language server starts the same way in both. loom lsp speaks the Language Server Protocol over standard input and output once it is given --stdio, and it treats the nearest folder that holds .loom/config.yaml as the project, the folder loom init sets up. A .loom folder alone makes no project. The standalone executable lives in ~/.loom/bin, and the home folder above it is not a project, so each editor looks for the configuration file rather than the folder. It needs a TypeScript compiler. VS Code sends its own, and every other editor relies on the server's search: the project's typescript package first, then the one installed beside Loom.
Neovim's server configuration
Neovim 0.11 configures a language server with a table its vim.lsp.config function takes, and it reads such tables from an lsp/ folder on its runtime path. The nvim-lspconfig project collects one file per server in that folder, so this file is written in its form: a short description for its generated documentation, then the table. The table names the command, the file type it serves, and how to find a project's root. Neovim matches a root marker against the names in each folder, and a name cannot reach inside .loom, so the table gives a root_dir function instead. It takes the nearest folder whose .loom holds a config.yaml, then the nearest Git repository, and otherwise starts the server with no root, as Neovim does for a loose file.
---@brief
---
--- https://github.com/athrio-com/loom
---
--- The Loom language server, which the `loom` command line ships:
---
--- ```sh
--- npm install -g @athrio/loom
--- ```
---
--- `loom lsp` finds TypeScript in the project, or beside Loom, to check
--- TypeScript sections against.
---@type vim.lsp.Config
return {
cmd = { 'loom', 'lsp', '--stdio' },
filetypes = { 'loom' },
root_dir = function(bufnr, on_dir)
local workspace = function(name, path)
local config = vim.fs.joinpath(path, name, 'config.yaml')
return name == '.loom' and vim.uv.fs_stat(config) ~= nil
end
on_dir(vim.fs.root(bufnr, workspace) or vim.fs.root(bufnr, '.git'))
end,
}Neovim's plugin
The rest of Neovim's setup is a plugin file, which Neovim runs at startup from any plugin/ folder on its runtime path. Adding packages/tree-sitter-loom/editors/neovim to the runtime path therefore sets up everything this chapter describes, and the server configuration beside it comes along.
A person sets Neovim up with loom nvim. That command compiles the grammar the CLI ships straight into Neovim's data folder, copies the queries beside it, and writes a plugin of its own with the same settings, so it needs neither this folder nor nvim-treesitter. The two files here are the other form, the one Neovim's own projects collect: nvim-lspconfig takes the server's configuration, and nvim-treesitter takes the address it fetches the grammar from.
Neovim does not yet know the .loom extension, so the plugin adds it as the loom file type.
vim.filetype.add({ extension = { loom = 'loom' } })nvim-treesitter installs a parser from a repository by name. Its own list does not hold Loom yet, so the plugin adds Loom to the list each time nvim-treesitter refreshes it, which it signals with a TSUpdate event. The grammar lives in a subfolder of Loom's repository, and so do its queries. :TSInstall loom then fetches the repository, compiles the committed parser and scanner, and installs the queries.
vim.api.nvim_create_autocmd('User', {
pattern = 'TSUpdate',
callback = function()
require('nvim-treesitter.parsers').loom = {
install_info = {
url = 'https://github.com/athrio-com/loom',
location = 'packages/tree-sitter-loom',
queries = 'packages/tree-sitter-loom/queries',
},
}
end,
})Neovim colours a buffer with tree-sitter only when asked. The plugin asks for every loom as it opens, and turns on the language server it configured.
vim.api.nvim_create_autocmd('FileType', {
pattern = 'loom',
callback = function()
vim.treesitter.start()
end,
})
vim.lsp.enable('loom')Helix's languages
Helix learns a language from a languages.toml file, its own built-in one or a user's in the configuration folder. A language entry gives the name, the scope, the file extension, the root markers, and the servers that serve it. Helix 25.07 tests a root marker by joining it to each folder above the file, so the marker can name the configuration file itself, .loom/config.yaml. The injection pattern lets Markdown hand a fence marked loom to this grammar.
A person sets Helix up with loom helix. That command writes the same language and server entries into the user's languages.toml and compiles the grammar the CLI ships straight into Helix's runtime folder, so the user's file needs no grammar entry. This file is the other form, the one Helix's own project collects for a language it supports, where Helix fetches and builds the grammar itself.
The server entry names the command. The grammar entry tells Helix where to fetch the grammar and which subfolder to build. Helix pins a grammar to a commit and fetches it again only when the pin changes, so a branch name would hold whatever commit the first fetch found. The revision names the commit that last changed the grammar, and moves when the grammar does.
{{ revision = "bbadbf24e9cb8b131a2f3fa5e390e9929960bd01" }}
[[language]]
name = "loom"
scope = "source.loom"
file-types = ["loom"]
roots = [".loom/config.yaml"]
injection-regex = "^loom$"
language-servers = ["loom"]
[language-server.loom]
command = "loom"
args = ["lsp", "--stdio"]
[[grammar]]
name = "loom"
source = { git = "https://github.com/athrio-com/loom", rev = "::[revision]", subpath = "packages/tree-sitter-loom" }Helix reads a language's queries from runtime/queries/loom, and in this form hx --grammar fetch followed by hx --grammar build fetches and compiles the grammar. The queries for that folder are the Helix copies in editors/helix/queries/loom, which name a frontmatter key's colour the way Helix themes do, and they are the copies loom helix installs.