Skip to content

Add AL (Microsoft Dynamics 365 Business Central) language support - #16161

Open
SShadowS wants to merge 1 commit into
helix-editor:masterfrom
SShadowS:add-al-language
Open

Add AL (Microsoft Dynamics 365 Business Central) language support#16161
SShadowS wants to merge 1 commit into
helix-editor:masterfrom
SShadowS:add-al-language

Conversation

@SShadowS

@SShadowS SShadowS commented Aug 8, 2026

Copy link
Copy Markdown

Adds AL, the language of Microsoft Dynamics 365 Business Central.

The language

AL is what Business Central extensions are written in. It is a recognised language in GitHub Linguist (since 2020), and already has editor support in VS Code (Microsoft's own extension), Zed and Sublime Text.

The grammar

SShadowS/tree-sitter-al, pinned to v3.2.1.

  • published to crates.io, npm and PyPI
  • CI runs the upstream tree-sitter/parser-test-action on Linux, macOS and Windows, fuzzes the external scanner, and validates the queries with ts_query_ls
  • 1472 corpus tests, external scanner in C99, ABI 15
  • also carried by tree-sitter-language-pack

Queries

The queries come from the grammar repository, which targets nvim-treesitter, so they are adapted rather than copied.

indents.scm is rewritten. Two things make AL different from most C-likes here:

  • AL is written Allman style, and the grammar keeps the braces inside the declaration node instead of exposing a brace-delimited node like C's compound_statement. So the declaration's scope already covers the line the opening brace sits on, and "{" has to outdent.
  • Nodes whose body is a begin/end block must not open a scope of their own, because the code_block opens one on the following line and the two stack into a double indent.

Brace-less if/while/for/foreach/with bodies use header-scoped captures in the same shape as the C query; repeat/until scopes like a block.

locals.scm definitions gained the suffixes Helix resolves against (local.definition.variable, .variable.parameter). A bare local.definition is a discard, so the file as inherited did nothing at all. Table fields, enum values and keys are deliberately left as discards, since they are object members rather than locals.

highlights.scm captures are renamed to the documented theme scopes: property to variable.other.member, number to constant.numeric.*, module to namespace, and the keyword.* and *.definition variants onto their Helix equivalents. The (ERROR) capture is dropped, since repainting half-typed code while typing is not wanted.

textobjects.scm loses the block.* captures, which Helix has no textobject for, and a repeat-body capture that made mif inside a loop select the loop rather than the enclosing procedure.

tags.scm is added, with definition.method mapped onto definition.function and local symbols removed, so the symbol pickers are useful.

injections.scm is added for comment injection.

folds.scm from the grammar repo is deliberately not included, since Helix does not load a folds query.

Verification

$ cargo xtask query-check al
Query check succeeded

$ cargo xtask indent-check al
Indent check succeeded

$ cargo xtask highlight-check al
Highlight check succeeded (6 assertions)

query-check fetched the grammar from the pinned revision and built it before compiling the queries, so it also confirms the pin resolves and the grammar builds and loads under Helix.

indent-check runs against the new tests/indent/al.al fixture, which covers objects, sections, fields, keys, var sections, begin/end, braced and brace-less if/else, while, for, repeat/until, case with an else branch, and procedures. It caught two real bugs during development that query-check cannot see, which is why the fixture is included rather than offered.

highlight-check runs against tests/query/highlights/al/constructs.al and pins the renamed captures, which are the riskiest part of this change.

cargo xtask docgen was run and the regenerated lang-support.md row is in the commit; re-running it produces no further change.

cargo test --workspace passes apart from test_treesitter_indent_rust_helix, which fails identically on master on this machine and is unrelated to this change (it re-indents Helix's own Rust sources, and the checkout has CRLF line endings).

No rainbows.scm is included; happy to add one if you would like the language to land with it.

AI disclosure

This change was prepared with AI assistance (Claude). The query adaptation was driven by the checks above, all run locally.

AL is the language of Microsoft Dynamics 365 Business Central. It is
recognised by GitHub Linguist, and already supported in VS Code, Zed and
Sublime Text.

Grammar: https://github.com/SShadowS/tree-sitter-al, pinned to v3.2.1.
It is published to crates.io, npm and PyPI, runs the upstream tree-sitter
CI workflows on Linux, macOS and Windows, has 1472 corpus tests, and
targets ABI 15.

The queries come from the grammar repository, which targets
nvim-treesitter, so they are adapted to Helix rather than copied:

- indents.scm is rewritten. AL is written Allman style and the grammar
  puts the braces inside the declaration node rather than in a separate
  brace-delimited node, so the declaration scope already covers the line
  the opening brace sits on; "{" therefore outdents. Nodes whose body is
  a begin/end code_block no longer open a scope of their own, since the
  code_block opens one on the following line and the two would stack.
  Brace-less if/while/for/foreach/with bodies use header-scoped captures
  the way the C query does, and repeat/until scopes like a block.
- locals.scm definitions gain the suffixes Helix resolves against
  (local.definition.variable, .variable.parameter). Bare
  local.definition is a discard, so the file previously did nothing.
  Table fields, enum values and keys are demoted to discards: they are
  object members, not locals.
- highlights.scm captures are renamed to Helix's theme scopes, so
  property becomes variable.other.member, number becomes
  constant.numeric.*, module becomes namespace, and the keyword.* and
  *.definition variants map onto their documented equivalents. The
  (ERROR) capture is dropped; repainting half-typed code is not wanted.
- textobjects.scm loses the block.* captures, which Helix has no
  textobject for, and the repeat-body capture that made mif inside a
  loop select the loop rather than the enclosing procedure.
- folds.scm is dropped: Helix does not load a folds query.
- tags.scm is added, with method mapped onto definition.function and
  local symbols removed, so the symbol pickers work.
- injections.scm is added for comment injection.

Verified with cargo xtask query-check al, indent-check al (against the
new tests/indent/al.al fixture, covering objects, sections, fields, var
sections, begin/end, brace-less and braced if/else, while, for,
repeat/until, case with an else branch, and procedures) and docgen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant