Paper
Swara: A LilyPond Toolkit for Neovim
Active

Installing and Using Swara

A Tree-sitter grammar, a Rust language server and a Neovim plugin for LilyPond, the plain-text music engraver, built from the ground up for speed.

lilypondneovimtree-sitterrustluamusic

01.What Swara Needs

Swara's colours, instant checks and tidying need nothing but Neovim and the swara program, which the plugin fetches for itself. Everything that engraves, previews or plays a score calls an outside program, and each one is optional in the sense that a missing tool is reported once and the rest keeps working.

Table 1. What each feature needs

ProgramNeeded forNotes
Neovim 0.12 or laterEverythingThe tests run on Neovim 0.12.6
LilyPondThe LilyPond check, builds, preview and playbackTested with LilyPond 2.26.0. Other versions should work, because the server reads LilyPond's commands from whichever LilyPond is installed
FluidSynth and a soundfontPlaybackMuseScore 4's MS Basic.sf3 is used when MuseScore 4 is installed in /Applications, and the soundfont setting names any other
MuseScore 4:Swara musescoreOpens the score's MIDI for slower practice
SkimA PDF that reloads after each build, on macOSWithout it the PDF opens in the default viewer
snacks.nvimDrawing the preview inside the terminalOptional. Without it the preview opens in the system's image viewer

Two of these terms deserve a definition. FluidSynth is a software synthesiser: it reads a MIDI file, which says only which notes play and when, and turns it into sound. To do that it needs a soundfont, a file of recorded instrument samples that decides what each note sounds like. A soundfont file ends in .sf2, or .sf3 for the compressed form.

On macOS, Homebrew, the common package manager for the Mac, installs every program in the table except snacks.nvim, which is a Neovim plugin.

Bash
brew install neovim lilypond fluid-synth
Bash
brew install --cask musescore skim

Playback keeps a single FluidSynth running from the first play until Neovim quits, and sends it commands through a pipe, a private channel between two programs on the same machine. So playback opens no network port.

02.Installing the Plugin

With the lazy.nvim plugin manager, one line installs Swara.

Lua
{ 'anirbanchakraborty-dev/swara', ft = 'lilypond', opts = {} }

The first string is the plugin's GitHub repository. ft = 'lilypond' tells lazy.nvim to load the plugin only when a LilyPond file opens, so it costs nothing at startup, and opts = {} runs the plugin's setup with every setting at its default.

The plugin needs the swara program, which holds the language server and the grammar. It uses a swara found on PATH, the list of folders the shell searches for programs, when there is one. Otherwise, the first time a LilyPond file opens, it downloads the release archive for its own version from the repository's GitHub releases into Neovim's data folder. It unpacks the archive only once its SHA-256 checksum matches the one listed in the release's SHA256SUMS file. A checksum is a short fingerprint computed from every byte of a file, so any change to the archive, whether a corrupted download or a substituted file, gives a different fingerprint and the archive is refused.

Release archives are built for five platforms: macOS on Apple silicon and on Intel, Linux on x86-64 and on ARM64, and Windows on x86-64. The tests run on macOS and Linux.

03.Installing the Program with Homebrew

On macOS and Linux, Homebrew can install the swara program instead of letting the plugin download it. Homebrew installs from taps, which are GitHub repositories of install recipes, and Swara's recipe lives in its own tap.

Bash
brew install anirbanchakraborty-dev/tap/swara

The plugin does not compare its own version with a swara it finds on PATH, so run brew upgrade swara after each plugin update to keep the two on the same release.

04.Letting nvim-treesitter Manage the Grammar

nvim-treesitter is not needed, because the plugin loads its grammar from the swara program. To have nvim-treesitter manage the grammar instead, for example so that other plugins can use it, register it in a User TSUpdate autocommand, as nvim-treesitter's main branch documents.

Lua
vim.api.nvim_create_autocmd('User', { pattern = 'TSUpdate',
callback = function()
require('nvim-treesitter.parsers').lilypond = {
install_info = {
url = 'https://github.com/anirbanchakraborty-dev/tree-sitter-lilypond',
revision = 'v0.1.0',
queries = 'queries',
},
}
end,
})

Then run :TSInstall lilypond once. Keep the revision at the grammar tag that matches the plugin's release, since Swara's queries are written for that grammar. A different LilyPond grammar installed under the name lilypond stops Swara's colours, and :checkhealth swara names the conflict.

05.Commands

Every command is a word after :Swara. A build works from the text in the editor, so a score can be engraved before it is saved.

Table 2. The Swara commands

CommandWhat it does
:Swara new guitar <name>Writes a guitar lesson <name>.ly in the current folder, with the sargam include beside it, and opens it
:Swara new piano <name>Writes a piano lesson the same way
:Swara new song <name>Writes a song lesson with its lyrics file and the lyrics include
:Swara buildBuilds the PDF and MIDI from the text in the editor, saved or not
:Swara openOpens the last build's PDF
:Swara previewShows the score as an image inside the editor
:Swara playPlays the last build's MIDI through FluidSynth
:Swara stopStops playing
:Swara musescoreOpens the last build's MIDI in MuseScore

06.Keys

Neovim reserves a key called the local leader for shortcuts that belong to one kind of file. It is a backslash unless the maplocalleader setting changes it, and a shortcut is the local leader followed by one more key. Swara sets six of them, in LilyPond files only, and the keys setting turns them all off.

Table 3. The Swara keys, with the default local leader

KeysWhat they do
\oOpens the PDF
\vPreviews the score in the editor
\pPlays the score
\sStops playing
\mOpens the score in MuseScore
\bBuilds the PDF and MIDI

07.Settings

Every setting goes in opts for lazy.nvim, or in require('swara').setup({ ... }) otherwise. The defaults suit most uses.

Table 4. The Swara settings and their defaults

SettingDefaultWhat it does
auto_checktrueRuns LilyPond's own check each time typing pauses
format_on_savetrueTidies the file each time it is saved
keystrueSets the keys above in LilyPond files
soundfontMuseScore 4's MS Basic.sf3The .sf2 or .sf3 soundfont that playback uses
debounce_ms10How long Neovim waits after a change before sending it to the server, in milliseconds
preview_dpi120The resolution of the preview image
lilypondthe lilypond on PATHThe LilyPond program to use
include_paths{}More folders to search for \include files, after the file's own folder
release_base_urlthe repository's GitHub releasesWhere the plugin downloads swara from

08.Checking the Setup

:checkhealth swara reports the swara program and where it came from, the grammar, LilyPond, the command cache, FluidSynth and the soundfont, MuseScore and, on macOS, whether Skim reloads a changed PDF by itself. Each thing that is missing or switched off comes with its fix. The check only reads and changes nothing. Where Skim's own reload is off, it shows the two defaults write commands that turn it on, and Swara has Skim reload the PDF after each build in the meantime.

09.Building from Source

Building Swara needs Rust 1.98 and the grammar checked out beside it, as a folder named tree-sitter-lilypond next to the swara folder. The first command builds the program, the second runs the Rust tests and the third runs the plugin's tests in a clean, headless Neovim.

Bash
cargo build --release -p swara -p swara-parser
Bash
cargo test --workspace
Bash
nvim --clean --headless -l tests/run.lua

— Anirban

Hardware–software at the seam where compilers meet microcode.

FeedbackBook modePaper