Swara is a fast LilyPond toolkit for Neovim, built from the ground up. It has three parts: a new Tree-sitter grammar for LilyPond, a language server written in Rust that checks a score each time typing pauses, and a Neovim plugin that wires both in, opens the PDF in Skim, previews and plays a score, and creates guitar, piano and song lessons that print Indian sargam names and Bengali lyrics.
Opening a .ly or .ily file starts Swara's colours, folds and indent from its own grammar, and its language server, which does the following.
\include lines that name no file.\score, \book and \bookpart blocks as document symbols, for a picker or an outline.The instant checks come from Swara's own grammar, which does not yet read every corner of LilyPond. Of the 2,774 files in its test corpus that LilyPond accepts, 6 show a syntax error that LilyPond does not report. Three of them define new syntax while LilyPond runs them, which no grammar can know in advance, and three use rare forms inside \with blocks and override values. None of the 114 published scores from the Mutopia Project in that corpus shows one. Where the two disagree, LilyPond's own check, which runs once typing pauses, is the one to trust. docs/bench/verdict.md compares the grammar with the existing LilyPond grammar for Tree-sitter, which it beats on speed and on broken files but not yet on this count.
LilyPond runs any Scheme code a file contains, and that code can run programs and change files as the user. So opening a file never runs LilyPond. The first run for a file comes only after typing in it, saving it or asking for a build, each a deliberate act on that file, the same as pressing compile. A downloaded piece opened only to read is never run, and the auto_check setting turns the check while typing off altogether.
The plugin adds the rest. It shows the score as an image inside the editor, plays it through FluidSynth, opens it in MuseScore for slower practice, and writes new lessons from templates. A guitar or piano lesson prints the sargam name of each note (Sa, Re, Ga and so on) under the staff, in Bengali, Devanagari or Latin letters, counted from any chosen Sa. A song lesson takes lyrics typed in English letters and prints them in Bengali script, with the words as typed in smaller type beneath them, or either line alone. The converter is a general engine, and Bengali is the one script it carries a table for so far.
MS Basic.sf3 is used when MuseScore 4 is installed in /Applications, and the soundfont setting names any other.:Swara musescore.On macOS, Homebrew installs all of these but snacks.nvim.
brew install neovim lilypond fluid-synth
brew install --cask musescore skim
Playback keeps one FluidSynth running from the first play until Neovim quits and sends it commands through a pipe, so it opens no network port.
With lazy.nvim:
{ 'anirbanchakraborty-dev/swara', ft = 'lilypond', opts = {} }
The plugin needs the swara program, which holds the language server and the grammar. It uses a swara found on PATH first. Otherwise, the first time a LilyPond file opens, it downloads the release archive for its own version from this repository's GitHub releases into Neovim's data folder, and unpacks it only once its SHA-256 matches the release's SHA256SUMS. Release archives are built for macOS on Apple silicon and Intel, Linux on x86-64 and ARM64, and Windows on x86-64. The tests run on macOS and Linux.
On macOS and Linux, Homebrew can install the program instead, from anirbanchakraborty-dev/homebrew-tap:
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.
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 to use it in other plugins, register it from tree-sitter-lilypond in a User TSUpdate autocommand, as nvim-treesitter's main branch documents, and run :TSInstall lilypond once.
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,
})
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.
| Command | What 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 build |
Builds the PDF and MIDI from the text in the editor, saved or not |
:Swara open |
Opens the last build's PDF |
:Swara preview |
Shows the score as an image inside the editor |
:Swara play |
Plays the last build's MIDI through FluidSynth |
:Swara stop |
Stops playing |
:Swara musescore |
Opens the last build's MIDI in MuseScore |
Each key is set in LilyPond buffers only, after the local leader key, which Neovim's maplocalleader setting chooses and which is a backslash unless changed. The keys setting turns them all off.
| Key | What it does |
|---|---|
<localleader>o |
Opens the PDF |
<localleader>v |
Previews the score in the editor |
<localleader>p |
Plays the score |
<localleader>s |
Stops playing |
<localleader>m |
Opens the score in MuseScore |
<localleader>b |
Builds the PDF and MIDI |
Every setting goes in opts for lazy.nvim, or in require('swara').setup({ ... }) otherwise. The defaults suit most uses.
| Setting | Default | What it does |
|---|---|---|
auto_check |
true |
Runs LilyPond's own check each time typing pauses |
format_on_save |
true |
Tidies the file each time it is saved |
keys |
true |
Sets the keys above in LilyPond buffers |
soundfont |
MuseScore 4's MS Basic.sf3 |
The .sf2 or .sf3 soundfont that playback uses |
debounce_ms |
10 |
How long Neovim waits after a change before sending it to the server, in milliseconds |
preview_dpi |
120 |
The resolution of the preview image |
lilypond |
the lilypond on PATH |
The LilyPond program to use |
include_paths |
{} |
More folders to search for \include files, after the file's own folder |
release_base_url |
this repository's GitHub releases | Where the plugin downloads swara from |
: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 off comes with its fix. The check only reads. 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.
Each target comes from the design, and every one was met with LilyPond 2.26.0 on the machine described in docs/spikes/speed-basis.md. docs/bench/speed.md gives the fastest and slowest runs and how each figure is taken.
| Measurement | Median | Target |
|---|---|---|
| Colours after a keystroke | 0.19 ms | under 16 ms |
| Instant checks after an edit | 1.2 ms | under 50 ms |
| LilyPond check after the last edit, 100-bar song | 551 ms | under 600 ms |
| Tidy, 100-bar song | 1.1 ms | under 10 ms |
Completion after \ |
1.1 ms | under 20 ms |
| Server ready | 2.3 ms | under 100 ms |
| PDF after save, 4-bar lesson | 266 ms | about 300 ms |
| PDF after save, 100-bar song | 1,311 ms | about 1,500 ms |
| Play, first time, to the first note | about 1,060 ms | about 1,500 ms |
| Play, later, to the first note | about 4 ms | under 200 ms |
The LilyPond check has the least room: 330 ms of it is the pause Swara waits for typing to stop, and most of the rest is LilyPond itself. The two play figures run to the first note Swara sends. A first play sends it only once FluidSynth has opened its audio output, and FluidSynth's audio buffer adds a delay after that which was not measured.
The workspace expects the grammar repository checked out beside it, as ../tree-sitter-lilypond, and needs Rust 1.98.
cargo build --release -p swara -p swara-parser
cargo test --workspace
nvim --clean --headless -l tests/run.lua
Swara is released under the MIT licence, in LICENSE.
Two boundaries keep it that way. First, LilyPond's command descriptions, which completion and hover show, are GPL text, so Swara never ships them. The server reads them from the LilyPond installed on each machine the first time it needs them and keeps them in that user's cache folder. Second, no code, query, word list or description is copied from a GPL project, LilyPond, Frescobaldi, python-ly and nvim-lilypond-suite among them. The grammar, the queries and the test files were written from scratch for this project.