Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Mapal — VS Code support

Puts the actual Mapal logo on .mapal files, plus comment and bracket behavior.

package.json                 language registration + the file icon
language-configuration.json  // comments, bracket matching, auto-close
icons/mapal.svg               the mark, square variant

Why this and not an icon theme

VS Code has no "add one icon" API for file icon themes — a file icon theme is all-or-nothing, so shipping a Mapal-only one would blank the icons for every other file type in your project. That is why this uses contributes.languages[].icon instead (VS Code ≥ 1.66): the icon shows for .mapal files, and your existing icon theme keeps handling everything else. If your active icon theme does define an icon for .mapal, the theme wins — that is by design.

Install

Not published to the Marketplace, so build a .vsix and install it:

python3 editors/vscode/package-vsix.py
code   --install-extension editors/vscode/mapal-lang-0.1.0.vsix   # or:
cursor --install-extension editors/vscode/mapal-lang-0.1.0.vsix

Then restart the editor. Verify it actually registered — the message alone is not proof:

code --list-extensions | grep mapal-lang

If the code command is not on your PATH, use the binary inside the app: "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code".

Do not just copy the folder in

Dropping this directory (or a symlink to it) into ~/.vscode/extensions does not work on current VS Code or Cursor. They keep extensions.json in that directory as the authoritative registry and do not scan for unregistered folders, so a hand-placed extension is ignored in silence — no error, no icon, no highlighting, indistinguishable from a broken extension. Only the CLI install writes the registry entry.

That is also why package-vsix.py exists: vsce is the normal way to build a .vsix and it needs npm, whereas a .vsix is just a ZIP holding extension.vsixmanifest, [Content_Types].xml and an extension/ directory. The script builds one directly and asserts the archive contains all three before claiming success.

Iterating on the extension

A CLI install copies the files, so editing the repo afterwards changes nothing in the editor. Either rebuild and reinstall:

python3 editors/vscode/package-vsix.py && code --install-extension editors/vscode/mapal-lang-0.1.0.vsix --force

or launch a throwaway window that loads the source directly, which is better for grammar work:

code --extensionDevelopmentPath="$(pwd)/editors/vscode" .

Checking it works

  1. Open a .mapal file — the status bar should read Mapal, not Plain Text. If it says Plain Text the extension is not loaded and nothing else matters.
  2. Command palette → Developer: Inspect Editor Tokens and Scopes, cursor on a token — should show source.mapal and a scope such as support.function.builtin.mapal.

Syntax highlighting

syntaxes/mapal.tmLanguage.json covers comments, strings, guard arrows (split-scoped — chrome as an arrow, discriminant by what it is), labels, fn declarations, builtins, bindings, call position, arrows, numbers, types, operators, and the reserved-and-rejected category.

Run editors/test.sh after any edit — it asserts both editors together.

Scope names have to be ones themes actually style. Labels were first scoped entity.name.label, which is semantically right and looked broken: VS Code's default themes give it #C8C8C8 against an editor foreground of #CCCCCC, a 4/255 difference that renders as plain text. They are now keyword.control.label.mapal (#C586C0), which is also what the Vim file does — it links labels to Label, a Statement-family group. When picking a scope, check it against the theme rather than only against the grammar.

Three things to know if you edit it.

The rule that decides outcomes is earliest match wins, ties broken by listed order — not listed order alone. This bites specifically: a rule like (?<=->)\s*(ident) begins its match at the whitespace after the arrow, one column before the keyword rule that matches the identifier itself, so it wins on position however the patterns are ordered. That is why #binding and #call-position carry an explicit negative lookahead listing every builtin and ret; without it, -> println; scoped println as a variable. scope_test.py asserts that exclusion covers every builtin, so adding one to #keywords and forgetting the exclusion fails the test rather than quietly mis-coloring.

It has the opposite precedence to the Neovim file: TextMate takes the first matching pattern, Vim the last. So this file is ordered most-specific first and ../nvim/syntax/mapal.vim least-specific first. A rule added to one goes at the other end of the other.

And it is less capable in one specific way: x -> name; is lexically identical whether name is a new variable or a call to a no-value function. The Vim file resolves it by scanning the buffer for fn declarations; TextMate has no way to know what has been declared, so here every -> name; reads as a binding. A terminal call to your own function will be colored as a variable.

That divergence is the argument for not maintaining two grammars forever. The real fix is one LSP server driving both editors from the compiler's own parse (ADR-0008), at which point both hand-written grammars become fallbacks.