Skip to content
Technically Business Central
Zed Rust LSP AL

AL Development in Zed: Full Language Support Without VS Code

The Zed AL extension brings full IntelliSense, diagnostics, debugging, and code insight to Zed — built in Rust, no .NET startup cost.

B

Brad Fullwood

14 min read

I’ve been writing AL in VS Code since Microsoft shipped the first version of the extension in 2017. It works, mostly. But the editor startup is slow, the AL Language extension takes another few seconds to warm up after that, and on Linux the experience ranges from “slightly degraded” to “completely broken” depending on which release you’re on. After spending a year building AL tooling in Rust anyway, I had all the pieces I needed to do this properly. The Zed AL extension is the result.

It runs as a native binary. No .NET startup, no Electron overhead on top of Electron. Hover responses come back in under 500 microseconds. Completions in under 3ms. Workspace symbol search across 30,000 symbols in under 5ms. These aren’t approximations — they’re numbers I can measure because the hot paths don’t cross a managed runtime boundary.

What the extension actually does

Language intelligence

IntelliSense in the extension covers the full AL surface: object types, fields, procedures, triggers, system functions, dotnet types from your NuGet dependencies, and built-in BC objects. When you hover over Rec."Posting Date", you get the field’s type, its table, its caption, and any documentation comment on it. When you hover over a system function like CALCFIELDS, you get the signature and a description pulled from the BC documentation corpus.

Go-to-definition works across your workspace and into referenced symbols. For built-in BC objects — things that exist only as compiled symbols in the system app packages — the extension generates virtual documents on demand. Pressing F12 on Customer opens a synthetic .al file showing the object’s fields, keys, procedures, and triggers as readable AL source. You’re not looking at decompiled IL. The document is assembled from the symbol metadata, formatted as real AL, and presented in a read-only buffer.

Find all references and rename both work. Rename has XLIFF awareness: when you rename a field or procedure that has caption properties, the extension finds the corresponding trans-units in your .xlf translation files and updates the source strings there too. It doesn’t touch target translations — those are yours to manage — but it keeps your source in sync automatically.

Diagnostics

Diagnostics run in two phases. The first phase is instant: as you type, tree-sitter parses the current document and reports syntax errors. No round-trip to any compiler. If you have a mismatched begin/end or a malformed trigger signature, you see it immediately in the Problems panel.

The second phase is async. After a configurable debounce (default 800ms after the last keystroke), the extension invokes CodeCop and AppSourceCop through the .NET bridge. These run in-process through the CLR hosted by netcorehost — not a subprocess, not a separate daemon. The diagnostics come back and merge into the panel alongside the tree-sitter results, tagged by source so you can distinguish a syntax error from a CodeCop rule violation.

The two-phase model matters because CodeCop isn’t fast. It can take a second or two on a large object. Running it synchronously on every keystroke would make the editor feel sluggish. The tree-sitter layer keeps feedback instant for the things it can catch, and the CodeCop layer handles the rest without blocking you.

Code actions

There are 10 quick fixes and 4 source actions.

The quick fixes handle the mechanical corrections that show up constantly in AL work: adding missing var keywords on procedure parameters, converting IF statements to CASE where the pattern matches, replacing deprecated system calls with their current equivalents, adding ObsoleteState and ObsoleteReason to deprecated objects, inserting missing caption properties, and a handful of others that come up from CodeCop violations frequently enough to be worth automating.

The source actions operate on whole documents: organize using namespaces (sort, deduplicate, remove unused), add missing caption properties to all fields in a table, extract selected code into a new local procedure, and add XML documentation stubs to all public procedures that are missing them.

Formatting

The formatter is opinionated and not configurable in most respects. It follows the AL formatting conventions from Microsoft’s own style guide: 4-space indentation, begin on the same line as the statement that precedes it, blank lines between procedure declarations, specific casing rules for keywords. There’s a zed-al.formatter.trailingNewline setting and a few options around blank line handling, but the core style isn’t up for debate. This is intentional. Consistent formatting across a team is worth more than individual preference, and having one canonical style means you stop arguing about it.

You can format on save or invoke it manually. It runs locally in the Rust binary, so there’s no latency.

Semantic tokens

The extension provides 16 semantic token types: objects, fields, procedures, triggers, variables (local, global, parameter), system functions, built-in types, labels, report columns, query columns, data item names, XML port nodes, action references, option values, and namespace identifiers. Zed’s theme system maps these to colors, so you can configure exactly what your AL code looks like at the token level.

Combined with the tree-sitter grammar (which handles the syntactic highlighting), you get two layers: fast structural coloring from tree-sitter, and semantic meaning from the LSP. The combination handles edge cases that pure tree-sitter grammars struggle with — distinguishing a local variable from a field from a procedure call when they have the same name, for instance.

Build, publish, and debug

The extension exposes Zed tasks for every operation you’d normally reach for a terminal to run. Build, clean, publish, RAD publish, run tests, download symbols — all available as tasks with configurable arguments. The tasks use the same launch configuration format as VS Code’s launch.json, so if you’re migrating an existing project the settings translate directly.

Debugging goes through a full DAP proxy. Zed’s debug adapter protocol support connects to the proxy, which translates to the AL debug protocol that Business Central’s service tier speaks. Breakpoints, stepping, variable inspection, call stack — all of it works. Snapshot debugging is also supported: you can attach to a snapshot, step through recorded execution, and inspect state from a specific point in time without needing a live session.

NuGet symbol download is handled natively. The extension reads your app.json dependencies, resolves them against configured NuGet feeds (including BC’s symbol packages), and downloads .app files into your .alpackages directory. No manual feed management in VS Code’s NuGet panel.

Performance details

The symbol index is backed by DashMap — a sharded concurrent hashmap that lets multiple threads read without contention. During workspace initialization, .app packages are memory-mapped in parallel. memmap2 on Linux and Windows, with a fallback to buffered reads on file systems that don’t support it. The parallel load means a workspace with 50 symbol packages initializes in about the same time as one with 10.

Tree-sitter parsing is incremental. When you edit a document, only the changed regions get re-parsed. For a 2000-line codeunit where you change one procedure, the parse overhead is microseconds, not milliseconds.

The workspace symbol search (triggered by ] in Zed’s command palette) does a fuzzy match across all indexed symbols. 30,000 symbols in under 5ms is the benchmark I run in CI. The fuzzy algorithm is a port of VS Code’s built-in scorer, which prioritizes acronym matches and start-of-word matches over arbitrary substring hits.

Hover: under 500μs. Completions: under 3ms for the ranked list. These are measured from request receipt to response send in the LSP server, excluding any network or IPC overhead.

The Insight Engine

Beyond the standard LSP features, the extension ships an analysis layer I call the Insight Engine. It builds four graphs over your workspace:

Call graph: which procedures call which others, across object boundaries. You can ask “what calls this procedure” and get a tree of callers, not just a flat list of references. Useful for understanding the blast radius before you change a signature.

Event graph: which publishers exist, which subscribers are bound to each, and which of those subscribers are in external extensions. Cross-extension event tracing works when you have multiple extensions loaded as dependencies — you can see the full subscriber tree even when the subscriber code is in a different app.

Object graph: dependency relationships between objects, based on field types, record references, and procedure calls. Shows you which objects are tightly coupled and which are cleanly isolated.

Table relation graph: the foreign-key-like relationships in BC — TableRelation properties, CalcFormula lookups, SetRange/SetFilter call patterns. Useful for understanding how data flows through the system.

Dead code detection runs over the call graph. Any procedure that has no callers and is not an event subscriber, trigger, or exported API gets flagged. The threshold is configurable — you can suppress warnings for procedures with specific attributes, or for entire objects.

Dependency impact analysis answers the question “if I change this, what else is affected?” Given a procedure or field, it traces the call graph and event graph forward to find everything that directly or transitively depends on it.

Linux support

This is the part I’m most personally invested in. Microsoft’s AL Language extension has had Linux issues for years. The .NET runtime hosting, the Mono fallback, the debugger adapter — various pieces have been broken or degraded at various times. The extensions-for-open-VSX build is often a version or two behind, and some features never land there at all.

The Zed AL extension has no such problems. The binary is compiled for Linux as a first-class target. The test suite runs on Linux in CI. Debug support works because the DAP proxy is pure Rust — there’s no managed component that depends on platform-specific .NET hosting behavior. If you’ve been putting up with broken hover or missing debug support on Linux, this is the reason I built this.

Settings and snippets

There are 59 settings. They cover LSP behavior (diagnostic debounce, hover detail level, completion filtering), build configuration (default targets, server URL, credential handling), insight engine toggles (which graphs to build, dead code thresholds), formatter options, and debug adapter settings.

There are 23 snippet files organized by object type and context. Table fields, report request pages, codeunit structures, event publisher and subscriber templates, interface implementations — the things you type repeatedly, already written and ready to expand. The snippet library is a separate file from the main extension so you can audit it or contribute additions.

AI integration

The extension registers slash commands that work in Zed’s AI panel. /al-explain takes the current selection and asks the configured model to explain what the code does in BC context. /al-refactor proposes a refactoring with awareness of AL idioms. /al-test generates a test codeunit stub for a selected procedure. The slash commands pass relevant context — the current object type, the symbol index metadata for referenced symbols, the BC documentation for involved functions — so the model has enough to give useful answers rather than generic ones.

Getting started

Install the extension through Zed’s extension panel (search “AL”), open a folder containing an app.json, and the extension activates. It downloads the al-lsp binary for your platform from GitHub releases on first run. There’s no separate install step for the language server.

If you’re on Linux and you’ve been dealing with the VS Code AL extension’s rough edges, try it. If you’re on Windows and just want faster responses and a cleaner editor, try it. The source is on GitHub under the al-tools organization.

Back to Blog
Share:

Related Posts