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 --lspStartup
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
| Diagnostics | Errors and warnings, as you type |
| Go to definition | Jump to where a label, constant or macro is defined |
| Find references | Every place a name is used |
| Hover | What a name is worth, or what an instruction costs |
| Completion | Instructions, directives, and your own names |
| Workspace symbols | Search every symbol in the project |
| Document symbols | This file’s outline, with local labels under the routine they belong to |
| Document highlight | Every use of the name under the cursor, in this file |
| Rename | A name and its uses, across the project |
| Call hierarchy | What reaches a routine, and what it reaches |
| Code actions | A quick fix where a diagnostic’s hint names a replacement |
| Document links | An INCLUDE’s file name, clickable |
| Folding ranges | MACRO, STRUCT, ENUM, MODULE, INTERFACE, REP and !test bodies |
| Inlay hints | A constant’s value beside the name |
| Signature help | The 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 2An 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.
Document Links, Folding and Inlay Hints
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 highlighting | A TextMate grammar and semantic tokens for the fantasm language |
| Snippets | For .asm and .inc files |
| Build task | Task type fantasm, taking a file and an optional target |
| Problem matcher | Named fantasm, so build errors land in the Problems panel |
| Debugging | Breakpoints 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:
| Setting | Default |
|---|---|
0x1de.fantasm.serverPath | fantasm — an absolute path if it is not on your PATH |
0x1de.fantasm.serverArgs | ["--lsp", "--nologo"] |
0x1de.fantasm.target | zxnext — 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.tomlconfigures for the build, if not for the editor.