Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How did we get here?

If you did not expect the author to be some random guy that had rarely even been using Macaulay before, then don’t worry as so did I. After I’ve opened iot once to make some calculations, I’ve noticed that it was probably the last alive language on Earth with no language server implemented yet. I thought it must be a fun hobby project to work on next month. 11 months later the alpha build is here, together with every single shower thought of anything that could potentially be build from that.

After all I ended up with way more of them than I’ve expected, so that releasing them one by one would be a bit of a pain, so I decided to bundle them all together into one bundle where I could manage tyhem all together and youy could browse them all together.

Tools included

  • Language server: static code analyzer including, type-checker, formatter, linter and fully featured language server. It’s written in Rust and fully independent on M2, so you can use it even on Windows.
  • spectral-m2: A new shell preserving the good old experience with some new refreshed TUI interface and new renderer supporting rich highlighting and customisable styling.
  • tree-sitter: Fast, incremental parser with rich syntax highlighting
  • MarkdownDocs: Generating documentation as markdowns and mdbook-based web builder, giving documentation a fresh look.
  • macaulay2.fun: A web service hosting the docs build with mdbook
  • VSCode extension: Porting LSP and tree-sitter to work in VSCode
  • mdbook-tsitter: Preprocessor for mdbook that uses tree-sitter, making it properly highlight m2 code blocks.
  • Neovim plugin: Mostly for ease configuration of LSP and tree-sitter, with a few extra features like inline cell evaluation.
  • m2-syn Rust crate with native parser generating fully typed AST, syntax tree traversal, AST renderer

Tools in late WIP stage

What is still lacking

Proper emacs support and integration. As I’ve never used emacs, the bundle was developed possibly in parallel with the popular emacs developer environment. This doesn’t mean that emacs compatibility is an actual issue, but rather I just don’t feel competent to provide a proper support for it, and that I may have duplicate some systems that already exist there for ages.

Who it’s for

Every m2 user is likely to find something that will make his life easier - trying out the language server is probably a good starting point.

Quickstart

Language server

Requirescargo
Platformslinux, macOS, Windows
Successfully tested onArch
Documentationspectralflux.dev/decs/m2-ls.html
Repositorygithub.com/AlexanderGolys/m2-ls.git
crates.iocrates.io/crates/m2-ls

The m2-ls language server is cross platform binary Rust crate with no additional dependencies needed to run the binary. To install it from cargo toolchain run

cargo install m2-ls

To run it from source code, clone the repository and run

cargo run --bin m2-ls

To configure the client in code editors, see LSP setup

Linting and formatting

Linter and formatter used in m2-ls are also available as standalone binaries. After installing the crate, you can run them in terminal using

m2-fmt [file] 
m2-lint [file]

VS Code extension

Search for the Macaulay2 LSP extension and you’re done.

Documentation · Marketplace · Repository

m2.nvim

RequiresNeovim 0.11+, M2 for running code
Platformslinux, macOS, Windows
Successfully tested onArch
DocumentationNvim plugin
Repositorygithub.com/AlexanderGolys/m2.nvim

With lazy.nvim add

{
  "AlexanderGolys/m2.nvim",
  dependencies = { "nvim-treesitter/nvim-treesitter" },
  opts = {},
}

The plugin doesn’t start the language server by itself, see LSP in Nvim for the few lines that do.

Running code

<leader>mo opens M2 in a split, <leader>mc sends the expression under the cursor (or the selection) to it.

spectral-m2

Requirescargo, M2
Platformslinux, macOS
Successfully tested onArch
Documentationspectral-m2
Repositorygithub.com/AlexanderGolys/spectral-m2
crates.iocrates.io/crates/spectral-m2

spectral-m2 runs your installed M2 underneath, so M2 needs to be on the PATH. To install it run

cargo install spectral-m2 --locked

and start it with spectral-m2 (or just sp2) instead of M2. Typing / on an empty line lists all commands.

Fonts

Icons need a Nerd Font. Without one, set SPECTRAL_M2_BASIC_GLYPHS=1.

MarkdownDocs

RequiresM2, mdbook
Platformslinux, macOS
Successfully tested onArch
DocumentationMarkdownDocs
Repositorygithub.com/AlexanderGolys/MarkdownDocs

Clone the repository and install the package from M2

installPackage("MarkdownDocs", FileName => "/path/to/MarkdownDocs.m2")

mdbook-tsitter

Requirescargo, mdbook 0.5, git, C compiler
Platformslinux, macOS, Windows
Successfully tested onArch
Documentationspectralflux.dev/mdbook-tsitter
Repositorygithub.com/AlexanderGolys/mdbook-tsitter
crates.iocrates.io/crates/mdbook-tsitter

To install it run

cargo install mdbook-tsitter

and enable it in book.toml

[preprocessor.tsitter]

First build

Grammars are compiled on first use, which is why git and a C compiler are needed. After that they’re cached.

ProceduralMacros

RequiresM2 1.26.05+
Platformslinux, macOS
Successfully tested onArch
DocumentationProceduralMacros
Repositorygithub.com/AlexanderGolys/ProceduralMacros

Clone the repository and install the package from M2

installPackage("ProceduralMacros", FileName => "/path/to/ProceduralMacros.m2")

m2-syn

Requirescargo
Platformslinux, macOS, Windows
Successfully tested onArch
Documentationdocs.rs/m2-syn
Repositorygithub.com/AlexanderGolys/m2-syn
crates.iocrates.io/crates/m2-syn

m2-syn is a library crate. To add it to your Rust project run

cargo add m2-syn

Inspecting a file

To see the syntax tree of a file, clone the repository and run

cargo run --example inspect -- file.m2

tree-sitter-macaulay2

Requirescargo, npm or pip
Platformslinux, macOS, Windows
Successfully tested onArch
Documentationtree-sitter-macaulay2
Repositorygithub.com/AlexanderGolys/tree-sitter-macaulay2
crates.iocrates.io/crates/tree-sitter-macaulay2
npmnpmjs.com/package/tree-sitter-macaulay2
PyPIpypi.org/project/tree-sitter-macaulay2

The grammar is published for Rust, Node and Python

cargo add tree-sitter tree-sitter-macaulay2
npm install tree-sitter-macaulay2
pip install tree-sitter-macaulay2

Editors

To use the grammar for highlighting in an editor, see Setting up tree-sitter.

See also

List of project specific documentations

mdbook builds of Macaulay2 docs

My other projects

Contact

spectral-m2

A new face for the M2 prompt. It’s still the good old REPL underneath, just with a TUI that doesn’t feel like 1995.

cargo install spectral-m2 --locked
sp2

What’s nicer

  • Highlighting as you type, and Enter on an unfinished expression just continues it on the next line.
  • Rendered output: TeX becomes actual maths in the terminal (stacked fractions, ℚ if you like), long outputs fold, and help pages show up inline with clickable links.
  • Side views of your session: just the code, just the values, printed output, or the syntax tree of what you typed.
  • History with prefix search: type ide, press Up, get only things starting with ide. Shared live between open shells.
  • Sessions: your code is saved on exit and /resume brings it back (by re-running it). /save and /load work with any .m2 file.
  • Colour schemes: catppuccin, tokyo night, gruvbox, everforest, rosé pine, kanagawa… light and dark.

Your init.m2 and installed packages load as usual.

Getting around

Type / on an empty line and everything is listed. The ones you’ll use most:

Alt+C / V / P / TView code / values / printed output / syntax tree
Shift+EnterNew line
Alt+RRestart M2
Ctrl+OSettings (theme lives here)
Alt+KRebind keys
Ctrl+CQuit

You’ll need

  • M2 on your PATH
  • A terminal with true colour. Kitty, WezTerm, Ghostty, foot and the like are perfect.
  • A Nerd Font for the icons, or SPECTRAL_M2_BASIC_GLYPHS=1

Not yet

You can’t interrupt a running computation yet (a cell gives up after 5 minutes), and it’s one evaluation at a time.

Repository · crates.io

m2-ls

The language server: the thing that started all of this. It gives any editor with an LSP client errors as you type, hover docs, go-to-definition, rename, completion, formatting and a type checker.

It’s written in Rust and never runs Macaulay2. Everything is static, and M2’s documentation is baked into the binary, so it starts instantly, works without M2 installed (yes, even on Windows), and can’t get stuck on a Gröbner basis.

What it does

  • Diagnostics: syntax errors, code M2 rejects or quietly ignores, type errors, style nits. More on the linter page.
  • Type checker: infers types without running anything, from the return types M2 declares for its own methods. If it isn’t sure, it keeps quiet: a type error it shows should be real.
  • Hover: docs for builtins and for your own code, plus the inferred type (how it works).
  • Navigation: definition, references, implementations, type hierarchy, symbols, and jumping into M2’s library sources if you have M2 installed.
  • Rename across files, inlay type hints, signature help, semantic highlighting.
  • Quick fixes and refactors, e.g. L.1 → L#1 or “flatten nested if”.
  • Formatting: see formatter.

It follows needsPackage, load, needs and friends, knows what a package exports, and checks TEST blocks like normal code.

n = 5
if n then print "yes"   -- warning: if condition must have type `Boolean`, but this has type `ZZ`

Install

cargo install m2-ls

You get m2-ls, m2-lint and m2-fmt. Then hook up your editor. In VS Code skip all that, since the extension brings the server along on Linux, macOS and Windows.

Settings

Under the m2-ls section of your editor’s LSP config:

{
  "m2-ls": {
    "diagnostics": { "disabled": ["unused-binding", "T02"] },
    "formatting": { "indent": 4 },
    "inlayHints": { "expressionTypes": true },
    "sources": { "paths": ["~/opt/macaulay2"] }
  }
}

The full list is in the repository README.

Known gaps

  • Things that only exist at runtime (value "x", symbols made on the fly) stay unknown.
  • Completion is sparse on purpose: it shows up when there are a few good candidates, not 400.
  • Only files reachable from what you open are indexed, not the whole project folder.

Under the hood

Parsing is m2-syn on top of the tree-sitter grammar, incremental as you type. Builtin docs and signatures come from an index extracted from Macaulay2’s own documentation.

m2-ls is MIT. The only exception is that embedded documentation, which stays under Macaulay2’s GPL.

Repository · crates.io

Formatter

Part of m2-ls, and also a standalone m2-fmt.

It fixes spacing, derives indentation from the syntax tree and wraps long lines where the syntax allows. It doesn’t change what your code does: dropping redundant parentheses and such is left to quick fixes.

f=x->(if x>0 then 1 else 2)        -- before
f = x -> (if x > 0 then 1 else 2)  -- after

In the editor, just use “format document”. On the command line:

m2-fmt file.m2            # in place
m2-fmt --check file.m2    # exit 1 if not formatted, handy in CI
m2-fmt --stdout file.m2   # print instead of writing

Files with syntax errors are left alone.

Knobs

Under m2-ls.formatting (or --config settings.json for m2-fmt):

  • indent, useTabs: default to your editor’s settings, 4 spaces on the CLI
  • softLineWidth / hardLineWidth: preferred and maximum line width, 78 / 90
  • controlFlowLayout: compact, multiline or multilineCompactElse (default)
  • compactFactorOperators: 2*x instead of 2 * x
  • breakAfterSemicolon: new line after every ;

Linter

Also part of m2-ls, and on the command line as m2-lint.

Rules come in four groups:

  • X, syntax: parse errors, and code that parses but means something else (L.1 is not L#1)
  • E, M2 semantics: things M2 rejects or silently ignores, like installing a method on a plain function, = where := is needed, or return outside a function
  • T, types: an if on something that can’t be Boolean, a declared return type that contradicts the code
  • S, style: unused locals, lowercase option keys, if (c) then, null checks that could be ??

A lot of them come with a quick fix. Unused locals starting with _ are fine.

Each rule has a code and a name (S02 = unused-binding); the full list is in the m2-ls README. Turn rules off or down in settings:

{ "m2-ls": { "diagnostics": { "disabled": ["S06"], "warningsAsHint": ["unused-binding"] } } }

In CI

m2-lint file.m2
m2-lint --deny-warnings --config settings.json file.m2
t.m2:2:4: warning[T02]: if condition must have type `Boolean`, but this expression has type `ZZ`

Exit code 1 on errors (or warnings, with --deny-warnings).

MarkdownDocs

An M2 package that turns documentation into Markdown. It works on the installed docs of any package, so the output is exactly what help shows, just in a format that can go anywhere: an mdBook site, a GitHub wiki, an Obsidian vault.

needsPackage "MarkdownDocs"
mdBook "Graphs"     -- build the Graphs docs as a book and open it in the browser

Markdown files

writeMdDocs("Graphs", "docs")

writes docs/Graphs/ with an index and one page per documentation node. LinkedPackages => true also writes the packages it links to, and LinkStyle => "wikilink" gives [[links]] for Obsidian. Examples come with the output stored at install time; nothing is re-run.

As a library

render markdown help "ideal"

markdown turns any M2 hypertext into a small Markdown tree, render makes it text, and in between you can reshuffle it however you like.

Install

installPackage("MarkdownDocs", FileName => "/path/to/MarkdownDocs/MarkdownDocs.m2")

Websites are on the next page: mdBook-based builder.

Repository

mdBook-based builder

MarkdownDocs can also lay the docs out as an mdBook: search, dark mode, a sidebar that makes sense, M2 highlighted by mdbook-tsitter, maths with KaTeX.

mdBook "Graphs"

builds it, serves it locally and opens the browser, like viewHelp. The server goes away when M2 does.

The book layout

The docs are grouped the way you’d read them, not the way they’re stored: all methods of a function are chapters of that function’s page, and a package’s methods on someone else’s function (say degree of a graph) get a page in that package. mdDocs gives the plain one-page-per-node layout instead.

Your package as a website

writeMdBook("MyPackage", "docs", LinkedPackages => true)
writeBookFiles "docs/MyPackage"
mdbook build docs/MyPackage -d "$PWD/site/MyPackage"

Build every package’s book side by side so the links between them work, and site/ can go straight to GitHub Pages.

Needs mdBook; mdbook-tsitter and mdbook-katex are optional but recommended.

Refreshed web service

All of the Macaulay2 docs, rebuilt with the tools from this bundle and put online.

  • search across every package at once
  • dark mode, and a sidebar you won’t get lost in
  • M2 code actually highlighted
  • the book layout, with methods grouped under their function

Three tabs at the top:

  • Reference: every object in M2, generated from M2 itself
  • Original: M2’s own docs, one book per package
  • Guide: the Macaulay2 Book

The docs are extracted from a running M2, converted to Markdown and built with mdBook; the same extracted data feeds the language server’s hover. Want the same for your package? See the mdBook-based builder.

The content itself belongs to the Macaulay2 and package authors.

Macaulay2 Book

Work in progress. Not much to read yet.

The reference documentation is great once you know what you’re looking for, but it’s a reference: generated page by page, one per function. The Macaulay2 Book is meant to be read front to back: a guide to the language and its ideas, the way the Rust Book is for Rust.

It’s the “Guide” tab of the web service.

Proposal: towards new API

This is a proposal, not something you can install. Opinions very welcome.

Writing M2 docs today means a separate doc ///…/// block (or a document {…} list) far away from the code, in a hypertext DSL of its own. It works, but it’s a chore, so a lot of code ends up documented badly or not at all.

While building the language server I ended up needing docs in a different shape anyway: as data, attached to the code, and in Markdown. The next pages are about what that looks like and where I think it could go:

How LSP builds its hover

The server never runs M2, so hover docs have to come from somewhere else.

Builtins come from an index extracted once from Macaulay2’s own documentation and compiled into the server. Every function, type and method has a Markdown page there, plus its signatures and return types (which also feed the type checker). On hover, the signature is narrowed down to the method that’s actually being called.

Your code gets:

  • the inferred type and what the thing is (local, global, method, option, …)
  • the methods you installed on it
  • your doc comment, if there is one; see Inline docs

So a function you wrote two minutes ago already has a useful hover, and adding one comment line above it makes it as good as a builtin’s.

Inline docs and annotation

The language server picks up plain comments as documentation. No new syntax, just comments in the right spot, written in Markdown.

Above an assignment: consecutive -- lines right before it, no blank line between.

-- Number of edges of `G` between the vertices in `S`.
-- See also [[inducedSubgraph]].
edgesWithin = (G, S) -> #edges inducedSubgraph(G, S)

First thing in a function body: a -* … *- block.

edgesWithin = (G, S) -> (
    -* Number of edges of `G` between the vertices in `S`. *-
    #edges inducedSubgraph(G, S)
)

The whole package: a -* … *- block at the top of the file, before newPackage.

[[name]] links to another symbol: it’s clickable in the editor, and it gets updated when you rename that symbol. Code spans and fenced blocks are highlighted as M2.

Annotations that the type checker reads are the return types you can already declare in M2, f Type := ReturnType => x -> …. If it can work one out, the server offers to add it for you.

Inline docs and annotation

The language server picks up plain comments as documentation. No new syntax, just comments in the right spot, written in Markdown.

Above an assignment: consecutive -- lines right before it, no blank line between.

-- Number of edges of `G` between the vertices in `S`.
-- See also [[inducedSubgraph]].
edgesWithin = (G, S) -> #edges inducedSubgraph(G, S)

First thing in a function body: a -* … *- block.

edgesWithin = (G, S) -> (
    -* Number of edges of `G` between the vertices in `S`. *-
    #edges inducedSubgraph(G, S)
)

The whole package: a -* … *- block at the top of the file, before newPackage.

[[name]] links to another symbol: it’s clickable in the editor, and it gets updated when you rename that symbol. Code spans and fenced blocks are highlighted as M2.

Annotations that the type checker reads are the return types you can already declare in M2, f Type := ReturnType => x -> …. If it can work one out, the server offers to add it for you.

mdbook-tsitter

mdBook highlights code with highlight.js, which has never heard of Macaulay2, so M2 in a book was just grey text. This preprocessor highlights code blocks with real tree-sitter grammars at build time instead. This book uses it.

Not M2-only: it knows 300+ languages, and they look better too, because they’re parsed rather than regex-guessed.

cargo install mdbook-tsitter
# book.toml
[preprocessor.tsitter]

That’s the whole setup; fences tagged m2 (or anything else) are highlighted. Grammars are compiled on first use, so it needs git and a C compiler once.

Extras: notreesitter on a fence falls back to highlight.js, pane="Title" puts blocks side by side, and mdbook-tsitter css > theme/highlight_ts.css gives you the stylesheet to tweak. It follows mdBook’s light and dark themes.

Repository · Docs · crates.io

VSCode extension

The easy way in: Macaulay2 LSP comes with the language server inside, so installing it is the whole setup. Works on Linux, macOS and Windows, and you don’t need Rust or even Macaulay2 installed.

Search for Macaulay2 LSP in the Extensions view, or:

code --install-extension AlexanderGolys.macaulay2-language-server

Open a .m2 file and you’re done.

What you get

All the usual VS Code moves now work on M2:

Hoverdocs of builtins and your own functions, plus the inferred type
F12 / Shift+F12go to definition / find references
F2rename, across files
Ctrl+.quick fixes and refactors on the line
Ctrl+Spacecompletion: package names, option keys, symbols
Ctrl+Shift+O / Ctrl+Tsymbols in the file / in everything loaded
Format Documentthe formatter (Shift+Alt+F, Ctrl+Shift+I on Linux)
Problems paneleverything the linter and type checker found

Plus folding, highlighting of the symbol under the cursor, comment toggling with -- and -* … *-, and auto-closing brackets (including <| |>).

Highlighting comes from the server, not a regex grammar, so it knows what a name is: a function, a type, a local, an option. Colours show up a moment after the file opens.

Type hints are there but hidden, so they don’t clutter the code: hold Ctrl+Alt to see them.

Running code from the editor isn’t there yet; that’s Neovim-only for now.

Settings

Everything is under Macaulay2 in the Settings UI, or macaulay2.* in settings.json. Changes apply right away, except server.*, which needs a window reload.

Diagnostics

SettingDefault
macaulay2.diagnostics.enabledtrueall diagnostics on/off
macaulay2.diagnostics.disabled[]rules to hide, by name or code

The Settings UI offers every rule with a short explanation, so you don’t need to remember that S06 is ring-variable-naming.

Formatting

SettingDefault
macaulay2.formatting.indentWidtheditor’sspaces per level
macaulay2.formatting.useTabseditor’sindent with tabs
macaulay2.formatting.softLineWidth100width it prefers to wrap at, 0 = off
macaulay2.formatting.hardLineWidth100width it always wraps at, 0 = off
macaulay2.formatting.controlFlowLayoutmultilineCompactElsecompact, multiline or multilineCompactElse
macaulay2.formatting.compactFactorOperatorsfalse2*x instead of 2 * x
macaulay2.formatting.breakAfterSemicolontruenew line after ;

Inlay hints

SettingDefault
macaulay2.inlayHints.expressionTypesfalsealso types of arguments, subexpressions and call results
macaulay2.inlayHints.allKnownTypesfalseeven the obvious ones, like the type of a number literal

Server

SettingDefault
macaulay2.server.pathbundledyour own m2-ls build
macaulay2.server.arguments[]extra arguments for it
macaulay2.trace.serverofflog LSP traffic (messages, verbose) in the Output panel

A nice setup

{
  "[macaulay2]": {
    "editor.formatOnSave": true,
    "editor.inlayHints.enabled": "on"          // always show types
  },
  "macaulay2.diagnostics.disabled": ["ring-variable-naming"],
  "macaulay2.formatting.hardLineWidth": 90
}

Something’s off?

Set macaulay2.trace.server to messages, reproduce it, and grab the log from Output → Macaulay2 Language Server for the bug report.

Marketplace · Repository

Nvim plugin

m2.nvim makes Neovim feel at home with .m2 files and lets you run code from the buffer.

-- lazy.nvim
{
  "AlexanderGolys/m2.nvim",
  dependencies = { "nvim-treesitter/nvim-treesitter" },
  opts = {},
}

Neovim 0.11+.

What you get

  • filetype detection, comments, indentation, :make running the file through M2 with errors in quickfix
  • folding and indentation from the tree-sitter grammar
  • an M2 terminal next to your code, and keys to send code to it

The language server isn’t started by the plugin; it’s a few lines, see here.

Running code

Keys
<leader>moopen M2 in a split
<leader>mcsend the cell under the cursor (or the selection)
<leader>mlsend the current line
<leader>mthide / show the terminal (M2 keeps running)
<leader>mqclose it

A cell is just the top-level expression under the cursor, so no -- %% markers. A multi-line call goes as one piece.

Keys can be changed with opts.term.keys, or switched off with term = false.

Repository

Setting up tree-sitter

The tree-sitter grammar gives editors highlighting, folding and indentation that understand M2’s actual syntax.

You may not need this at all:

  • VS Code: the extension gets highlighting from the language server, nothing to set up.
  • Any editor running m2-ls: semantic highlighting from the server already covers most of what tree-sitter would. Tree-sitter adds instant highlighting before the server starts, folding and indentation.
EditorGuide
NeovimNvim
EmacsEmacs
Helix, Zednot written up yet

Setting up tree-sitter in Emacs

Not available yet. Emacs 29+ can load the grammar itself, but highlighting also needs a macaulay2-ts-mode that maps the grammar’s captures to faces, and nobody has written one.

Until then, Macaulay2’s own M2-mode handles highlighting, and the language server adds semantic highlighting on top.

If you’d like to write the major mode, the grammar’s queries/highlights.scm is the place to start; contributions are very welcome.

Setting up tree-sitter in Nvim

The grammar isn’t shipped with nvim-treesitter, so it has to be registered by hand. With the nvim-treesitter master branch:

vim.filetype.add({ extension = { m2 = "macaulay2" } }) -- not needed with m2.nvim

require("nvim-treesitter.parsers").get_parser_configs().macaulay2 = {
  install_info = {
    url = "https://github.com/AlexanderGolys/tree-sitter-macaulay2",
    files = { "src/parser.c", "src/scanner.c" },
  },
  filetype = "macaulay2",
}

Then install the parser:

:TSInstall macaulay2

Highlight queries live in the grammar repository’s queries/ directory and need to be on your runtimepath under queries/macaulay2/. The Nvim plugin ships folding, indentation and tags queries; with its parser option pointing at a local checkout of the grammar, it does the registration and the runtimepath part for you.

Setting up LSP

m2-ls is a standard language server, so any editor with an LSP client can use it. In VS Code there’s nothing to set up: the extension brings the server along.

Everywhere else, the recipe is the same:

  1. Put m2-ls on your PATH (see Install).
  2. Tell the editor to run m2-ls (it talks over stdio) for .m2 files.
  3. Use the directory with .git (or your package’s root) as the project root.

Settings can be passed either in initializationOptions or as workspace configuration under the m2-ls section. They are in m2-ls settings.

EditorGuide
VS CodeVSCode extension
NeovimNvim
EmacsEmacs
Helix, Zed, othersnot written up yet. The three steps above are all there is to it

Setting up LSP in Emacs

Untested. I don’t use Emacs, so this is the standard recipe rather than a verified setup. If you get it working (or it doesn’t), please let me know.

With eglot, built into Emacs 29+:

(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(M2-mode . ("m2-ls"))))

(add-hook 'M2-mode-hook #'eglot-ensure)

M2-mode is the major mode from Macaulay2’s own Emacs package; the server works alongside it.

Setting up LSP in Nvim

Neovim 0.11+ has everything built in, no nvim-lspconfig needed:

vim.filetype.add({ extension = { m2 = "macaulay2" } }) -- not needed with m2.nvim

vim.lsp.config("m2_ls", {
  cmd = { "m2-ls" },
  filetypes = { "macaulay2" },
  root_markers = { ".git" },
})
vim.lsp.enable("m2_ls")

To change server settings, add them to the same call:

vim.lsp.config("m2_ls", {
  -- ...
  settings = {
    ["m2-ls"] = {
      -- see the m2-ls configuration table
    },
  },
})

Check it’s running with :checkhealth vim.lsp while a .m2 file is open.

Inlay type hints are off by default in Neovim; turn them on with vim.lsp.inlay_hint.enable(true).

ProceduralMacros

Macros for Macaulay2, written in Macaulay2: code that writes code before it runs. Inspired by Rust’s procedural macros, and still very much an experiment.

needsPackage "ProceduralMacros"

runSource ///
$defineRecord PackageRelease(makePackageRelease; releaseName, releaseVersion) $
///

turns into

PackageRelease = new Type of BasicList
makePackageRelease = (releaseName, releaseVersion) -> new PackageRelease from {releaseName, releaseVersion}
releaseName = value -> value#0
releaseVersion = value -> value#1

expandSource shows the expansion without running it. Two macros come built in: defineRecord and inferReturns (which adds return types it can work out).

Your own

Declarative, a pattern and a template:

declMacro("double", "'e", "2 * ('e)")

'x is a metavariable, 'x:Number a typed one, '{…}+ repeats.

Or procedural, a function from the parsed input (a token tree from M2’s own parse) to new tokens:

installMacro("myMacro", ts -> ...)

For bigger things there are typed syntax-tree views with visitors and folds, and visualizeTree when you’re lost.

Install

installPackage "ProceduralMacros"

Needs M2 1.26.05+ (the first one with parse).

Watch out

  • load / needs don’t expand macros, so use value openIn "file.m2"
  • templates don’t add parentheses for you
  • the API will change

Prefer Rust? m2-syn does macros with a typed tree and quote-style templates.

Repository

m2-syn

syn for Macaulay2: a Rust crate that turns M2 code into a fully typed syntax tree, lets you walk and rewrite it, and prints it back out. The language server is built on it.

  • Typed tree: a SourceFile of Cells, and each construct is its own type (Expr::Lambda, Expr::If, …), so matching on it is checked by the compiler.
  • Round trip: an untouched file prints back byte for byte, comments included.
  • Visit, VisitMut, Fold: override the node you care about, the rest is walked for you.
  • quote_m2! / parse_quote_m2!: write M2 code with $(value) interpolation instead of gluing strings.
#![allow(unused)]
fn main() {
use m2_syn::nodes::Symbol;
use m2_syn::{ParsedFile, visit_mut::VisitMut};

struct Rename;

impl VisitMut for Rename {
    fn visit_symbol_mut(&mut self, symbol: &mut Symbol) {
        if symbol.text() == "left" {
            symbol.set_text("renamed");
        }
    }
}

let file = ParsedFile::from_source("left + right")?.edit(|cst| {
    Rename.visit_source_file_mut(cst);
});
assert_eq!(file.to_source(), "renamed+ right");
}
cargo add m2-syn
cargo run --example inspect -- file.m2   # in the repo: dump a file's tree

As a macro engine

The ProcMacro trait takes cells in and gives cells out, and with quote_m2! the output is written as M2. Right now these macros run from Rust, not from inside M2; for macros you call from M2 code, there’s ProceduralMacros.

The API is still moving between minor versions.

Repository · docs.rs

tree-sitter-macaulay2

The tree-sitter grammar: what editors use for highlighting, folding and indentation, and the parser at the bottom of everything Rust-side in this bundle.

It follows M2’s own precedence and newline rules, and part of the tests are generated from what M2’s own parse says, so it sees the code the same way M2 does.

It comes with highlight, fold, indent, injection (TEST /// … /// bodies are highlighted as M2) and tags queries, and bindings for Rust, Node and Python.

In your editor

See Setting up tree-sitter.

In your code

cargo add tree-sitter tree-sitter-macaulay2
#![allow(unused)]
fn main() {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_macaulay2::language())?;
let tree = parser.parse("R = QQ[x, y]", None).unwrap();
}

Same name on npm and PyPI. If you’d rather have typed syntax nodes than a generic tree, use m2-syn.

Repository · crates.io

m2-syn

syn for Macaulay2: a Rust crate that turns M2 code into a fully typed syntax tree, lets you walk and rewrite it, and prints it back out. The language server is built on it.

  • Typed tree: a SourceFile of Cells, and each construct is its own type (Expr::Lambda, Expr::If, …), so matching on it is checked by the compiler.
  • Round trip: an untouched file prints back byte for byte, comments included.
  • Visit, VisitMut, Fold: override the node you care about, the rest is walked for you.
  • quote_m2! / parse_quote_m2!: write M2 code with $(value) interpolation instead of gluing strings.
#![allow(unused)]
fn main() {
use m2_syn::nodes::Symbol;
use m2_syn::{ParsedFile, visit_mut::VisitMut};

struct Rename;

impl VisitMut for Rename {
    fn visit_symbol_mut(&mut self, symbol: &mut Symbol) {
        if symbol.text() == "left" {
            symbol.set_text("renamed");
        }
    }
}

let file = ParsedFile::from_source("left + right")?.edit(|cst| {
    Rename.visit_source_file_mut(cst);
});
assert_eq!(file.to_source(), "renamed+ right");
}
cargo add m2-syn
cargo run --example inspect -- file.m2   # in the repo: dump a file's tree

As a macro engine

The ProcMacro trait takes cells in and gives cells out, and with quote_m2! the output is written as M2. Right now these macros run from Rust, not from inside M2; for macros you call from M2 code, there’s ProceduralMacros.

The API is still moving between minor versions.

Repository · docs.rs

Work still in progress

Things I know are missing or half-done. If one of them is the reason you can’t use the bundle, tell me and it moves up the list.

  • Prebuilt binaries of the command-line tools (outside VS Code, it’s cargo install for now)
  • One installer for the whole bundle instead of a cargo install per tool
  • Emacs, Helix, Zed setup that someone has actually tested
  • spectral-m2: interrupting a running computation
  • m2-ls: comment-based lint suppression, scanning the whole project instead of only what you open
  • Macros: calling m2-syn macros from M2 code, a stable ProceduralMacros API
  • The Macaulay2 Book: needs actual chapters
  • New documentation API: still a proposal

How to contribute

Something broke

Open an issue on the tool’s repository (links are at the bottom of each page). Helpful to include: the tool and its version, your M2 version and OS, and a small .m2 snippet if you have one.

“This was confusing” or “I expected X” counts as a bug too.

It worked

Tell me anyway! Knowing someone uses it, and for what, is the best motivation there is.

Code

Every tool lives in its own repository with its own README on building and testing. Good starting points:

Licenses

m2-lsMIT, except the embedded Macaulay2 documentation (GPL)
spectral-m2GPL-3.0
mdbook-tsitterMIT or Apache-2.0
everything elseMIT