👻 VSCode Fantom LSP
This extension brings a rich developer experience to Fantom projects inside VSCode — syntax highlighting, real-time diagnostics, autocompletion, hover docs, go-to-definition, debugging with breakpoints and variable inspection, and more. It is powered by a Fantom LSP server written entirely in Fantom itself ( ⚠️ This is an unofficial, community-driven project. It is not affiliated with or endorsed by the Fantom language authors. ✨ Features✅ Implemented
📋 Requirements
🚀 InstallationFrom the VSIX (recommended)
Or install from the terminal:
From Source
⚙️ ConfigurationThe extension looks for configuration in two places, in priority order: 1.
|
| Key | Type | Default | Description |
|---|---|---|---|
fanPath |
string |
"" |
Absolute path to your Fantom installation directory (the folder that contains bin/fan). When empty or set to the placeholder value, the extension falls back to the FAN_HOME environment variable. |
finPath |
string |
"" |
Direct path to the fin executable (e.g. /path/to/fin-installation/bin/fin). When set, the debugger prefers this over fanPath for launch configurations. |
fanTargetBuild |
string |
"" |
The build target passed to fan build.fan <target> on every save. Leave empty to run the default target. |
debounceTime |
number |
2000 |
Milliseconds to wait after the last keystroke before running diagnostics. Lower values give faster feedback; higher values are gentler on large projects. Minimum: 100. |
enableUnusedImport |
boolean |
true |
When false, unused using import warnings are suppressed entirely. |
💾 The LSP server restarts automatically whenever
fan.config.jsonis saved.
2. FAN_HOME environment variable (global fallback)
If fanPath is not set (or left as the placeholder) in fan.config.json, the extension reads the FAN_HOME environment variable:
# ~/.bashrc / ~/.zshrc
export FAN_HOME=/opt/fantom-1.0.82
export PATH=$FAN_HOME/bin:$PATH
Resolution order (first wins):
fanPathinfan.config.jsonFAN_HOMEenvironment variable- Extension shows a warning and does not start
3. VSCode settings (settings.json)
Additional settings available through the VSCode UI or settings.json:
| Setting | Type | Default | Description |
|---|---|---|---|
fantom.javaPath |
string |
"" |
Full path to the java executable. Defaults to $JAVA_HOME/bin/java, then just java from PATH. |
fantom.suppressJavaWarning |
boolean |
false |
Suppress the startup warning when Java is not found. Enable this if you do not use the Fantom debugger. |
fantom.suppressWarningPopup |
boolean |
false |
Suppress the "Warnings found" notification popup shown at startup when the build produces warnings. |
fantom.useBuiltInLspPod |
boolean |
true |
Use the vscodeFantomLsp bundled with the extension (recommended). Set to false only if you have built and installed your own LSP pod. |
fantom.pedanticMode |
boolean |
false |
Warn on local variable declarations that lack an explicit type annotation (neither a type on the left side nor an as cast on the right). |
fantom.trace.server |
string |
"off" |
Trace LSP message traffic: "off", "messages", or "verbose". Useful for debugging the extension itself. |
fantom.format.enable |
boolean |
true |
Enable the Fantom formatter (Format Document / Format Selection). |
fantom.format.formatOnSave |
boolean |
false |
Automatically format Fantom files on save. |
fantom.format.indentSize |
number |
2 |
Spaces per indentation level (ignored when useTabs is true). |
fantom.format.useTabs |
boolean |
false |
Use tab characters for indentation instead of spaces. |
fantom.format.insertFinalNewline |
boolean |
true |
Ensure the file ends with a newline character. |
fantom.format.trimTrailingWhitespace |
boolean |
true |
Remove trailing whitespace from each line. |
fantom.format.maxBlankLines |
number |
1 |
Maximum consecutive blank lines to preserve. 0 keeps all blank lines. |
fantom.format.respectEditorConfig |
boolean |
true |
Let .editorconfig files override the settings above. |
fantom.format.collapseSpaces |
boolean |
true |
Collapse runs of two or more spaces into one in code regions (outside strings and comments). |
fantom.format.maxLineLength |
number |
0 |
Wrap lines that exceed this length. 0 disables wrapping. |
🛠️ Commands
Access these from the Command Palette (Ctrl+Shift+P):
| Command | Shortcut (Linux/Win · Mac) | Description |
|---|---|---|
| Fantom: Compile Project | Ctrl+F9 · Cmd+F9 |
Runs fan build.fan <target> and reports compiler errors with clickable file links |
| Fantom: Run All Tests | Ctrl+Shift+F9 · Cmd+Shift+F9 |
Runs all Fantom tests in the project |
| Fantom: Run All Tests in File… | Ctrl+F10 · Cmd+F10 |
Runs all test methods in the currently open .fan file |
| Fantom: Run Test Method… | Ctrl+Shift+F10 · Cmd+Shift+F10 |
Runs a single test method chosen from the current file |
| Fantom: Remove Unused Imports in File | — | Deletes all unused using lines in the current file and saves |
| Fantom: Remove Unused Imports in Project | — | Deletes unused using lines across every .fan file in the project |
| Fantom: Remove Unused Variables in File | — | Deletes unused variable declarations in the current file |
| Fantom: Remove Unused Variables in Project | — | Deletes unused variable declarations across the whole project |
| Fantom: Format Entire Project | — | Formats every .fan file in the project with a progress bar |
| Fantom: Create launch.json for Fantom Debugger | — | Creates .vscode/launch.json with default Launch and Attach configurations pre-filled from fan.config.json |
| Fantom: Rebuild debugger | — | (Re)compiles the debug adapter JAR from the bundled Java sources. Run this if the debugger is missing or broken |
🏗️ How It Works
VSCode Extension (TypeScript)
│
│ stdio (LSP protocol)
▼
vscodeFantomLsp ─── fan vscodeFantomLsp::Main
│
├── ProjectIndex (AST index of all .fan sources)
├── DiagnosticService (single-file + cross-file analysis)
├── CompletionService (dot-completion, keyword suggestions)
├── DefinitionService (go-to-definition)
├── HoverService (type signatures, pod docs)
└── fan build.fan (real compiler, error reporting)
VSCode Extension (TypeScript)
│
│ stdin/stdout (DAP protocol)
▼
fantom-debug-adapter.jar
│
│ JDWP / TCP
▼
JVM running your Fantom program
On activation the extension:
- Reads
fan.config.json(or falls back toFAN_HOME). - Deploys (or updates)
vscodeFantomLspinto$FAN_HOME/lib/fan/. - Spawns the LSP server via
fan vscodeFantomLsp::Main. - Registers the Fantom debug adapter so it is available immediately in the Run & Debug panel.
- Checks for Java (
JAVA_HOME→fantom.javaPathsetting →PATH). If Java is not found, a warning popup is shown with options to configure the path. If Java is found and the debug adapter JAR has not been built yet, it is compiled automatically from the bundled Java sources in the background with a progress notification. - The server indexes all
.fansources frombuild.fan'ssrcDirs, pre-loads available pods, runs diagnostics, and performs an initial build check — all in the background so the editor stays responsive.
Shadow directories
The extension never modifies your Fantom installation while it is running. Instead, it creates a shadow directory — a lightweight mirror of FAN_HOME placed in the system temp folder — and points the LSP server and the debugger at that instead.
Why
The LSP server holds vscodeFantomLsp.pod open as long as it runs. Without a shadow copy, a build command that tries to overwrite that file would fail with a "file in use" error on Windows. The shadow dir gives the server its own unlocked copy while the original stays free for builds.
What is in the shadow dir
| Path inside shadow dir | What it contains |
|---|---|
lib/fan/<main>.pod |
Real copy of the LSP pod — the original is never locked |
lib/fan/*.pod |
Hard links (Windows) or symlinks (Linux/Mac) to the real pods |
lib/java/ |
Junction (Windows) or symlink (Linux/Mac) to the real lib/java/; falls back to a full recursive copy if the junction cannot be created |
etc/sys/config.props |
Modified copy with java.options stripped to suppress JDWP output on the LSP stdout pipe |
etc/sys/<other> |
Copies (Windows) or symlinks (Linux/Mac) of the remaining etc/sys/ files |
etc/<other>/ |
Junction (Windows) or symlink (Linux/Mac) to each real etc/ subdirectory |
.fantom-lsp-owner |
Process ID of the VS Code extension host that owns the shadow dir |
The shadow dir is created at LSP server startup and deleted when the server stops or restarts.
On activation the extension also removes shadow dirs left behind by sessions that were killed before they could clean up (e.g. VS Code restarting to apply an update). A shadow dir is only removed when its owner process is no longer running, so several VS Code windows can use the extension at the same time without deleting each other's shadow dirs. Shadow dirs created by older versions of the extension have no owner file; they are only removed once they are more than 24 hours old.
Safe deletion on Windows
On Windows, fs.rmSync({ recursive: true }) follows directory junctions as real directories, which would delete the contents of the junction target — i.e. your Fantom installation. The extension guards against this through ShadowDir (vscode-fantom/src/shadowDir.ts), the single class responsible for all shadow dir lifecycle operations:
- Boundary check — refuses to delete any path that is not under the system temp directory.
- Junction unlink first — unlinks every junction and symlink before calling
rmSync, sormSynconly ever sees regular files and empty directories. - Abort on partial failure — if any junction cannot be unlinked on Windows, the
rmSynccall is skipped entirely. The shadow dir leaks in%TEMP%but your installation is never touched.
The same class is used by the debugger's launch-mode shadow dir.
Diagnostics lifecycle
- While typing — changes are debounced (
debounceTimems). No analysis runs until typing pauses. - After the debounce window — single-file analysis runs immediately for fast feedback. The file is compiled together with signature-only stubs of the other files of its pod (method bodies removed), so types, constructors and slots declared in other files resolve exactly as in a full pod build. Only the stubs reachable from the file are compiled; files whose declarations cannot be resolved (e.g. they depend on a pod that is not installed) are left out, and references to their types are relaxed instead.
- On save — a full project re-index, cross-file validation, and
fan build.fanall run together.
🖋️ Formatter
The extension includes a full source formatter for Fantom files, implemented inside the LSP server (FormatterService). It supports Format Document, Format Selection, and optional format on save.
Activating the formatter
- Format Document —
Shift+Alt+F(or Command Palette →Format Document) - Format Selection — select code, then
Ctrl+K Ctrl+F - Format on Save — enable
fantom.format.formatOnSavein settings (see below)
The formatter respects VS Code's standard
editor.formatOnSavesetting too: whenfantom.format.formatOnSaveis enabled, the extension hooks into the save pipeline independently so you do not need to enableeditor.formatOnSaveglobally.
What the formatter does
Indentation
Indentation is re-computed by tracking brace depth ({ / }). Braces inside string literals and // comments are ignored. The unit is controlled by fantom.format.indentSize (spaces) or fantom.format.useTabs (tabs).
Blank line limiting
Consecutive blank lines are reduced to at most fantom.format.maxBlankLines (default 1). Set to 0 to remove all consecutive blanks, or keep the default to allow one blank line between declarations.
Trailing whitespace and final newline
When fantom.format.trimTrailingWhitespace is true (default), trailing spaces and tabs are stripped from every line. When fantom.format.insertFinalNewline is true (default), a newline is added at the end of the file if one is missing.
Line-ending preservation
The formatter detects the dominant line ending in the file (LF, CRLF, or CR-only) and uses the same style in the output. A Windows CRLF file stays CRLF after formatting; a Unix LF file stays LF.
Space collapsing
When fantom.format.collapseSpaces is true (default), runs of two or more consecutive spaces are collapsed to a single space in code regions — spaces inside string literals and // comments are never touched.
All Fantom string literal types are handled:
| Type | Example | Spaces inside |
|---|---|---|
| Double-quoted | "hello world" |
preserved |
| Triple-quoted | """hello world""" |
preserved |
| Single-quoted (char) | ' ' |
preserved |
| Backtick (DSL / URI) | `path/to thing` |
preserved |
Example:
// Before
return val ? 0 : 1
// After
return val ? 0 : 1
Line wrapping
When fantom.format.maxLineLength is set to a positive value, lines that exceed that length are split automatically. The formatter looks for the best split point before the limit, in this priority order:
| Priority | Split point | Example |
|---|---|---|
| 1 | Comma inside parentheses / brackets | foo(a, b, c) → split after , |
| 2 | && or \|\| operator (any nesting depth) |
if (a && b) → split before && |
| 3 | Ternary ? surrounded by spaces (at depth 0) |
x ? y : z → split before ? |
| 4 | Word boundary inside a string literal | "long string" → split with + concatenation |
String literal splitting produces a concatenation expression:
// Before (exceeds maxLineLength)
msg := "hello world this is a very long string"
// After
msg := "hello world this" +
"is a very long string"
The split is at the last word-boundary space before the column limit. Backtick (DSL) strings are never split — their content (URIs, raw values) must remain intact. Continuation lines are indented one level deeper than the base line.
.editorconfig support
When fantom.format.respectEditorConfig is true (default), the formatter walks up the directory tree from the file being formatted and applies the first matching .editorconfig section. Supported properties:
.editorconfig key |
Maps to |
|---|---|
indent_style = tab |
useTabs = true |
indent_style = space |
useTabs = false |
indent_size / tab_width |
indentSize |
trim_trailing_whitespace |
trimTrailingWhitespace |
insert_final_newline |
insertFinalNewline |
max_blank_lines |
maxBlankLines |
The .editorconfig walk stops at root = true or at the workspace root, whichever comes first. Per-file .editorconfig values override the VS Code settings for that file only.
🐛 Debugger
The extension includes a full Debug Adapter Protocol (DAP) implementation that lets you set breakpoints, step through code, and inspect variables in Fantom programs running on the JVM.
Prerequisite: A JDK (not just a JRE) is required — the debugger uses JDI, which is part of the JDK tools (
jdk.jdimodule, available in JDK 9+).Debug adapter JAR: The JAR is not pre-built in the repository. On first activation the extension compiles it automatically from the bundled Java sources (requires JDK 11+). If the build is skipped or fails, use the Fantom: Rebuild debugger command from the Command Palette (
Ctrl+Shift+P) to trigger a fresh build.
How it works
The debug chain
VS Code ←DAP/JSON-RPC→ fantom-debug-adapter.jar ←JDWP/TCP→ JVM (your Fantom program)
- VS Code communicates with the debug adapter using the Debug Adapter Protocol over stdin/stdout.
fantom-debug-adapter.jar(bundled with this extension) translates those commands into JDI calls. JDI (Java Debug Interface) is available in every JDK.- The JVM exposes a JDWP server socket when started with
-agentlib:jdwp=.... The adapter connects to that socket to set breakpoints, read variables, and control execution.
How .fan source files map to running code
Fantom compiles .fan files to standard JVM .class bytecode. Two standard attributes enable source-level debugging:
SourceFile— records the short filename (e.g.MyService.fan) in each.classfile.LineNumberTable— maps each bytecode instruction back to its original Fantom line number.
These attributes are always emitted by the Fantom compiler with no special build flags needed.
What is and is not inspectable
| Item | Available | Notes |
|---|---|---|
this fields |
✅ | All instance fields shown under the "this" scope |
| Local variables | ✅ (best-effort) | Shown when LocalVariableTable is present; otherwise method parameters are shown |
| Variable types | ✅ | Proper Fantom type names (myPod::MyClass, sys::List, etc.) |
| Variable values | ✅ | Calls Fantom's toStr() — shows the actual value, not fan.sys.List@123 |
| Step over / into / out | ✅ | Works at the Fantom source line level |
| Line breakpoints | ✅ | Click the gutter in any .fan file |
| Watch expressions | ✅ | Supports varName, this.field, and a.b.c chains |
| Conditional breakpoints | ❌ | Not yet implemented |
Java configuration
The JVM must be started with the JDWP agent to allow debugger connections.
Option A — config.props (for fan and fin)
Edit <fanHome>/etc/sys/config.props:
java.options=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
| JDWP option | Meaning |
|---|---|
transport=dt_socket |
Use a TCP socket |
server=y |
JVM opens the server socket |
suspend=n |
Program starts immediately; debugger can attach at any time |
suspend=y |
JVM waits for a debugger before starting — useful for debugging initialization |
address=5005 |
TCP port |
If you already have other JVM flags on java.options, separate them with a space (not a comma):
java.options=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dfile.encoding=UTF8
Cleanup: Remove the
java.optionsline when done. Withsuspend=yleft in place the program will hang on startup waiting for a debugger.
Option B — JAVA_TOOL_OPTIONS environment variable
Works for attach mode when you start the process manually before connecting:
export JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
fin
Do not use
JAVA_TOOL_OPTIONSwith launch mode. In launch mode the JDWP agent is injected viajava.optionsin a temporary shadowconfig.props, which is scoped to that one JVM and never inherited by child processes. UsingJAVA_TOOL_OPTIONSin parallel causes a port conflict and abind failed: Address already in useerror.
Quick start — Attach mode (recommended for FIN)
Step 1. Enable JDWP in <fanHome>/etc/sys/config.props:
java.options=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Step 2. Start your program normally:
fin
# or: fan myPod::Main
You will see Listening for transport dt_socket at address: 5005 confirming the port is open.
Step 3. Create .vscode/launch.json:
Tip: Run Fantom: Create launch.json for Fantom Debugger from the Command Palette (
Ctrl+Shift+P) to generate this file automatically. The extension also offers to create it on first activation whenfan.config.jsonis present and nolaunch.jsonexists.
{
"version": "0.2.0",
"configurations": [
{
"type": "fantom",
"request": "attach",
"name": "Attach to Fantom / FIN",
"host": "localhost",
"port": 5005,
"sourceDir": "${workspaceFolder}"
}
]
}
Step 4. Press F5 to connect.
Step 5. Click the gutter next to a line in any .fan file to set a breakpoint.
VS Code tip: If gutter clicks don't place breakpoints, add this to
settings.json:"debug.allowBreakpointsEverywhere": true
Launch mode — VS Code starts the process
In launch mode VS Code starts the Fantom/FIN process for you when you press F5. You do not need to touch config.props or start anything manually.
What happens when you press F5
- The adapter picks a free TCP port automatically (defaults to 5005, falls back to any free ephemeral port if 5005 is already in use — e.g. by a running FIN server).
- A temporary shadow
FAN_HOMEis created in/tmp/fantom-debug-home-*/:- Every top-level entry from your real
FAN_HOMEis symlinked exceptetc/andvar/. etc/sys/config.propsis rewritten:java.optionsfrom the original is stripped (prevents double-JDWP conflicts) and a fresh JDWP agent line for the chosen port is injected.debug=trueis also added so any pods rebuilt withpreLaunchRebuildemit full local variable info.var/is a real directory with only sub-directories symlinked — lock files likevm.lockare created fresh, completely isolated from any running FIN server.
- Every top-level entry from your real
- Your executable is launched with
FAN_HOMEpointing to the shadow directory. - The adapter attaches JDI to the JDWP port and is ready for breakpoints.
- When you press Stop (or close the session), the entire process group is killed with
SIGKILLand the shadow directory is deleted.
Because JDWP is injected via
java.optionsin the shadowconfig.props— not viaJAVA_TOOL_OPTIONS— it affects only the launched JVM and is never inherited by child processes or the running FIN server.
Step 1. Create fan.config.json (if not already present)
{
"fanPath": "/path/to/fantom"
}
Step 2. Create .vscode/launch.json
Plain fan launcher (explicit mainClass required):
{
"version": "0.2.0",
"configurations": [
{
"type": "fantom",
"request": "launch",
"name": "Launch Fantom",
"fanExe": "/path/to/fantom/bin/fan",
"mainClass": "myPod::Main",
"sourceDir": "${workspaceFolder}"
}
]
}
fin launcher (already embeds its entry point — mainClass must be empty):
{
"version": "0.2.0",
"configurations": [
{
"type": "fantom",
"request": "launch",
"name": "Launch FIN",
"fanExe": "/path/to/fin-installation/bin/fin",
"mainClass": "",
"sourceDir": "${workspaceFolder}"
}
]
}
Why
"mainClass": ""?
Thefinscript wrapsfanlaunch Fan finStackHost "$@"— the pod name is hardcoded inside the script. Passing an extrafinStackHost::Mainargument after-noAuthconfuses FIN's argument parser. LeavemainClassempty and rely on the script's built-in entry point.
fin with -noAuth:
{
"type": "fantom",
"request": "launch",
"name": "Launch FIN (no auth)",
"fanExe": "/path/to/fin-installation/bin/fin",
"mainClass": "",
"launcherArgs": ["-noAuth"],
"sourceDir": "${workspaceFolder}"
}
With local variable support (preLaunchRebuild rebuilds your pod with debug=true before each session):
{
"type": "fantom",
"request": "launch",
"name": "Launch FIN (full debug)",
"fanExe": "/path/to/fin-installation/bin/fin",
"mainClass": "",
"launcherArgs": ["-noAuth"],
"sourceDir": "${workspaceFolder}",
"preLaunchRebuild": true
}
preLaunchRebuildrunsfan build.faninsourceDirusing the shadowFAN_HOME(which hasdebug=true) so the rebuilt pod containsLocalVariableTablebytecode attributes. Without this, only method parameters are visible in the Variables panel — local variables likeroom11,room12are invisible. Set it totruethe first time you debug and whenever you do a clean build; afterwards you can set it back tofalseto skip the overhead.
Step 3. Press F5
The Debug Console shows the exact command being run:
[Fantom Debug] Launching: /path/to/fin -noAuth
If you see unexpected extra tokens after your arguments, check the mainClass field — it must be "" for fin.
Step 4. Set breakpoints and click the gutter in any .fan file
VS Code tip: If gutter clicks don't work, add to
settings.json:"debug.allowBreakpointsEverywhere": true
Step 5. Stop debugging
Press the Stop button (red square) or Shift+F5. The launched process and all its children are terminated immediately via SIGKILL on Linux/macOS.
Launch configuration reference
launch
| Key | Type | Default | Description |
|---|---|---|---|
fanExe |
string | auto-detected | Path to fan or fin. Auto-filled from fan.config.json (finPath first, then fanPath/bin/fan) |
mainClass |
string | "" |
Fantom class to run, e.g. myPod::Main. Leave empty for fin — fin already embeds its entry point; passing an extra class name after launcher args confuses its argument parser. |
launcherArgs |
string[] | [] |
Arguments inserted between the executable and mainClass, e.g. ["-noAuth"] → fin -noAuth. These are passed to the launcher script itself, before the Fantom pod name. |
args |
string[] | [] |
Arguments appended after mainClass — passed to the Fantom program's main() method. |
sourceDir |
string | ${workspaceFolder} |
Root directory searched for .fan source files (used for source ↔ JVM class mapping). |
debugPort |
number | 5005 |
Preferred JDWP port. If the port is already in use the adapter automatically picks a free ephemeral port. |
noDebug |
boolean | false |
Launch without attaching the debugger (plain run). |
preLaunchRebuild |
boolean | false |
Run fan build.fan in sourceDir before launching, using a shadow FAN_HOME with debug=true. This makes local variables (beyond method parameters) visible in the debugger. Only needed after a clean/release build. |
attach
| Key | Type | Default | Description |
|---|---|---|---|
host |
string | localhost |
Hostname or IP of the running JVM |
port |
number | 5005 |
JDWP port to connect to |
sourceDir |
string | ${workspaceFolder} |
Root directory searched for .fan source files |
Source file mapping
When the JVM reports a stopped location it provides only the short filename (e.g. MyService.fan) and the JVM class name (e.g. fan.myPod.MyService). The adapter searches sourceDir in this order:
<sourceDir>/<podName>/fan/<File.fan>— standard Fantom pod layout<sourceDir>/<podName>/<File.fan><sourceDir>/fan/<File.fan>— single-pod workspace- Full recursive walk under
sourceDir— fallback for non-standard layouts
Set sourceDir to the root of your source tree. For a multi-pod workspace this is typically ${workspaceFolder}.
Building the debug adapter from source
The debug adapter JAR is built automatically the first time the extension activates, using the Java sources bundled at vscode-fantom/bundled-debug/java-src/ (requires JDK 11+, no Maven needed). You can also trigger a rebuild at any time:
- Command Palette (
Ctrl+Shift+P) → Fantom: Rebuild debugger - Or from a terminal:
cd vscode-fantom
bash debug-adapter/build.sh
This downloads Gson if needed, compiles all Java source files, and packages a self-contained fat JAR.
🐛 Troubleshooting
| Symptom | What to check |
|---|---|
| "Extension idle" on startup | The workspace has no build.fan, no .fan files, and no fan.config.json. Add one. |
| "fanPath not configured and FAN_HOME is not set" | Set fanPath in fan.config.json or export FAN_HOME in your shell profile. |
| "fan executable not found" | Make sure bin/fan (or bin/fan.bat on Windows) exists in your Fantom directory. |
| No completions / wrong completions | Check that your file is inside the srcDirs listed in build.fan. Files outside srcDirs are not indexed. |
| Build errors not appearing | Set fanTargetBuild in fan.config.json to match your build target (e.g. "compile"). |
| Diagnostics too slow / too fast | Tune debounceTime in fan.config.json (default 2000 ms). |
| LSP server crashes | Open the Fantom Language Server output channel in VSCode and look for error messages. Enable fantom.trace.server: "verbose" for full protocol traces. |
| Debug adapter JAR not found | The JAR is built automatically at startup. If that failed, run Fantom: Rebuild debugger from the Command Palette. Requires JDK 11+. |
| Breakpoints never hit | Confirm JDWP is enabled on the JVM (see Java configuration above). Check the port matches between config.props and launch.json. |
Variables show <not found> |
The variable is out of scope or the frame is no longer active. Check the Call Stack panel to select the correct frame. |
| Local variables not shown (only method params) | Rebuild your pod with debug=true: either set "preLaunchRebuild": true in launch.json, or manually build with debug=true in etc/sys/config.props. |
| Program hangs on startup | You left suspend=y in java.options. Change to suspend=n or remove the line. |
bind failed: Address already in use |
Port 5005 is taken by the running FIN server's own JDWP listener. In launch mode the adapter picks a free port automatically. In attach mode use a different port. Do not set JAVA_TOOL_OPTIONS when using launch mode. |
-noAuth or other launcher flags ignored |
Set "mainClass": "" in launch.json when using fin. The fin script already embeds its entry point; a non-empty mainClass is appended after your flags and confuses FIN's argument parser. |
| FIN process keeps running after Stop | Ensure you are using the latest JAR (rebuild with bash debug-adapter/build.sh). Launched sessions are always force-killed on disconnect. |
🤖 AI-Assisted Development
This project is developed with the assistance of AI coding tools.
All AI-generated code is reviewed, tested against the full Fantom test suite (fan build.fan test), and compiled clean before being committed.
🤝 Contributing
Contributions are welcome! The LSP server lives in src/fan/ and is written in Fantom. The VSCode extension glue code is in vscode-fantom/src/.
# Run Fantom tests
fan build.fan test
# Run extension grammar tests
cd vscode-fantom && pnpm run test:grammar
📜 License
👻 Happy coding with Fantom!