The Language Server

FantASM is also a language server, so an editor can show you errors as you type, jump to a label’s definition, rename one across the project, and tell you what an instruction costs in T-states. It is the same binary — no separate download, and no second copy of the assembler that might disagree with the one building your program.

fantasm --lsp

Startup

The server speaks JSON-RPC over stdin and stdout, which is how every LSP client expects to talk to one.

--lsp is handled before the banner would be printed, and logging goes to stderr, so stdout carries protocol and nothing else. --nologo is harmless but does nothing here.

An editor starts this for you; you never run it by hand except to check the binary works.

Capabilities

DiagnosticsErrors and warnings, as you type
Go to definitionJump to where a label, constant or macro is defined
Find referencesEvery place a name is used
HoverWhat a name is worth, or what an instruction costs
CompletionInstructions, directives, and your own names
Workspace symbolsSearch every symbol in the project
Document symbolsThis file’s outline, with local labels under the routine they belong to
Document highlightEvery use of the name under the cursor, in this file
RenameA name and its uses, across the project
Call hierarchyWhat reaches a routine, and what it reaches
Code actionsA quick fix where a diagnostic’s hint names a replacement
Document linksAn INCLUDE’s file name, clickable
Folding rangesMACRO, STRUCT, ENUM, MODULE, INTERFACE, REP and !test bodies
Inlay hintsA constant’s value beside the name
Signature helpThe operand forms an instruction accepts

Diagnostics

The file is assembled on every keystroke and the result reported in place — the same errors and warnings fantasm build produces, from the same code.

A diagnostic lands in the file it came from. An error inside an included file is reported against that file, at its own line, not against the include line that pulled it in.

Only errors and warnings are marked. Build tracing — the Info: lines fantasm -v prints, such as which file an include resolved to — never reaches the editor, since there is no way to ask for it there and a mark on correct code is worse than no mark at all.

!message and a print in a !rhai block are yours rather than the assembler’s, so they still arrive, in the language server’s output pane with the file and line they came from. They do not underline your source.

Hover

A label or constant answers with its value and where it was defined:

Constant: MY_CONST = 100 (0x64)
test.asm, line 2

An instruction answers with its timing, including the forms that have two answers:

Instruction: JR
Timing: 12/7 T-states (taken/not taken)
Instruction: LDIR
Timing: 21/16 T-states (repeated/final)

That is the same timing data assert_cycles checks against in Testing.

Completion

Instructions and directives are offered from a fixed list; labels, constants and macros come from your source as it stands. . triggers a completion, so typing Gfx. offers that module’s members — see Modules.

Signature Help

Triggered by a space or a comma, so it appears as you start an operand and again after each one.

Rename

Renames a name and every use of it, across the project rather than the open file. Your editor asks first, and a position that is not a name it can rename says so instead of offering an edit that would do nothing.

A local label is renamed within its own routine. .loop under draw and .loop under clear are different labels, and renaming one leaves the other alone. Writing jp draw.loop from elsewhere is a use of the first.

Call Hierarchy

What reaches a routine, and what that routine reaches. Your editor draws it as a tree you can open downwards.

It is a reference hierarchy, and each entry says so. The graph records that a name was reached from a scope, not how — a call, a jp, an address loaded into hl, and a name in a db are the same edge. Read it as what mentions this, which is what makes it useful for finding out whether anything still does.

Code Actions

Where a diagnostic’s hint names a replacement, it is offered as a quick fix. E1003 on a name a module cannot see suggests the @NAME form; a deprecated spelling (W1103) offers the one that replaced it.

Nothing is offered for a diagnostic whose hint is advice rather than a replacement, since there would be nothing to apply.

An INCLUDE’s file name is a link, resolved the same way the assembler resolves it — so a link that does not open is the same missing file the build would report.

MACRO, STRUCT, ENUM, MODULE, INTERFACE, REP and !test bodies fold. Folding reads the text rather than the assembled program, so a block inside a false IFDEF still folds.

A constant’s value is shown beside its name as an inlay hint.

VSCodium and VS Code

Install 0x1DE from Open VSX (twistedraven.0x1de). It starts the language server for you and adds the rest of the environment around it:

Syntax highlightingA TextMate grammar and semantic tokens for the fantasm language
SnippetsFor .asm and .inc files
Build taskTask type fantasm, taking a file and an optional target
Problem matcherNamed fantasm, so build errors land in the Problems panel
DebuggingBreakpoints and stepping under Bizmuth — see Debugging a Build

Three commands: Assemble (FantASM), Run on Bizmuth, and Restart FantASM Language Server.

Settings, if the defaults do not suit:

SettingDefault
0x1de.fantasm.serverPathfantasm — an absolute path if it is not on your PATH
0x1de.fantasm.serverArgs["--lsp", "--nologo"]
0x1de.fantasm.targetzxnext — passed as -t by the build task

Both spellings there are the ones 2.0 retired. -t still works and warns once (W1103), so an extension-driven build now carries a deprecation line until 0x1DE moves to -d. The initializationOptions key stays target deliberately — it is the protocol the extension already speaks, and renaming it would break every client at once.

The extension is BSD-3-Clause, and its source is at codeberg.org/TwistedRaven/0x1DE.

Other Editors

Any LSP client will do. The command is fantasm --lsp, and the file extensions are whatever your project uses — .asm, .inc, .z80.

Neovim’s built-in client, as an example:

vim.lsp.start({
  name = 'fantasm',
  cmd = { 'fantasm', '--lsp' },
  root_dir = vim.fs.dirname(vim.fs.find({ 'fantasm.toml', '.git' }, { upward = true })[1]),
  init_options = { target = 'zxnext' },
})

init_options is optional where your fantasm.toml sets device — see below. Without either, the server assembles as a bare zx48, and a Next project shows E1016 Z80n extended instructions are not enabled on every nextreg and mul while the build succeeds on the same lines. What the client sends is read once at startup, so changing it means restarting the server; the project file is re-read as you type.

You get diagnostics, hover, completion, rename, call hierarchy and the rest — everything in the table above your client implements — but none of the highlighting, snippets or debugging, which are the extension’s rather than the server’s.

Project Discovery

The project root is the nearest directory at or above the open file containing fantasm.toml, .git or Cargo.toml. Nearest wins, so a fantasm.toml inside a checkout beats the .git above it.

include and incbin are then searched for in two places: the open file’s own directory, and that project root.

It is worked out per file, so a tree holding several projects gives each file the project it is in. Open hello/src/hello.asm and then console/src/console.asm and the second is searched under console/, whichever you opened first.

If nothing above a file is marked as a project, the folder your editor was opened on is used instead.

Settings It Honours

fantasm.toml is read, and its [assembler] section applies to what the editor shows you. include_dirs is added to the two directories above, so a project keeping headers in a sibling directory resolves them in the editor as it does on the command line. device, z80n, cspect, case_insensitive and origin each change what a diagnostic says, and all of them arrive. So does [defines].

A [[target]] answers for the file named by its main. Two sources in one directory can belong to different targets and get different settings — which is why this is worked out per file rather than sent once by your editor.

A file that is no target’s main — a header, or anything reached only through an include — takes [assembler] alone.

Anything your editor sends wins. A target in initializationOptions overrides the project file’s, the same way -d on the command line does. That key keeps its name — it is the protocol the extension already speaks, and renaming it would break every client at once.

Which File Answered

The language server’s output pane names it, once per document rather than per keystroke:

boot.asm → /home/you/SpectNext/console/fantasm.toml (target console); searching …

The bracket says which of four things happened: (target console) for a target’s main, (target console, included by src/main.asm) for a file that target reaches, (on its include path) for one a project finds through include_dirs without any target naming it, and (no target claims it) for one sitting under a project file nothing reaches it from.

Worth looking at when a diagnostic surprises you — no fantasm.toml above it and the wrong fantasm.toml produce the same symptom otherwise. A file that will not parse is reported there as a warning with the reason, and the editor falls back to defaults rather than going quiet.

Errors in an Included File

A file a target reaches is analysed as part of that target. Open a fragment that uses constants its parent defines, or continues from an org set elsewhere, and you see what the build sees — not E1003 for names that are perfectly fine. The output pane names the unit that answered:

palette.inc → /home/you/SpectNext/console/fantasm.toml (target console, included by src/main.asm)

A file no target reaches is assembled on its own, which is the one case where the editor can still show you a name the build would supply. A header nothing includes yet, or a file outside every project, is in that position; the output pane says so.

Limitations

No formatting. The server advertises exactly the capabilities in the table above, and an editor offering a command outside that list will find nothing behind it.

Not every editor implements every capability it is offered. Call hierarchy works in VSCodium and is absent from Zed’s UI, for example — the server answers either way, and what you see is the client’s decision.

Further Reading

  • Diagnostics — reading the messages the editor is showing you.
  • The Project File — what fantasm.toml configures for the build, if not for the editor.

Documentation