Skip to content

Latest commit

 

History

153 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flix (IntelliJ Plugin)

Build Code Inspection IntelliJ Platform License

Flix language support and breakpoint debugging for the wstein/flix-fork Flix compiler build, for IntelliJ-based IDEs.

Completion, diagnostics and hover come from the same flix lsp language server the official VS Code extension downloads -- no reimplementation -- via LSP4IJ's LSP client. Debugging runs on IntelliJ's own JVM debugger rather than a separate debug protocol, so one session covers Flix and every other JVM language in the same process: step from Flix into Java or Kotlin and back, with each language's own breakpoints, source navigation and evaluator.

Why LSP4IJ, not the native LSP API

IntelliJ's own LSP Client API covers language features, but is available only to plugins in IntelliJ-based IDEs under conditions LSP4IJ does not impose, and running both clients against one server would mean two clients for one language. LSP4IJ is therefore the sole LSP client here.

LSP4IJ also ships a generic DAP client. This plugin does not use it: debugging goes through IntelliJ's own JVM debugger instead (ADR 0002), which is what lets one session cover Flix and every other JVM language in the same process.

What works, and what's still unverified

  • Language features (LSP): live-verified -- syntax highlighting, diagnostics and completion via flix lsp, through LSP4IJ.
  • Flix language support (PSI): a real Language("Flix"), file type, Grammar-Kit/JFlex parser and PSI, syntax highlighter, brace matcher, commenter, quote handler and folding, adopted from intellij-flix per ADR 0001. The adopted grammar parses 427 of 427 compilable files in the upstream Flix corpus, up from 211 at adoption; see the parser corpus evaluation. This replaced the bundled TextMate fallback, which a real file type deactivates.
  • Debugging: IntelliJ's own JVM debugger, per ADR 0002. The gate is Green -- Flix and Java breakpoints in one session, stepping in both directions, mixed stack navigation, Java locals and evaluation, and a Step Over that stops on the next line of the same Flix definition rather than descending into everything the line calls.
  • Project tasks: build, check, test, run, clean, doc, build-jar, build-fatjar, build-pkg, outdated and init, under Tools > Flix and as a Flix Task run configuration. Not terminal wrappers: each runs through the platform's own execution machinery, so Stop kills the compiler, the exit code reaches the Run widget, the task can be re-run and saved, and the line numbers in a diagnostic are links into the source. Registered from the language module, so they stay available in an IDE with neither LSP4IJ nor the Java plugin. The subcommand names are checked against flix --help by FlixTaskTest rather than reviewed — an unknown one is not rejected by the compiler, it is demoted to a file argument and reported as Unrecognized file extension.
  • Show AST: Tools > Flix > Show AST. ShowAstProvider has been in the compiler all along, reachable only through lsp/showAst on Flix's VS Code protocol, which negotiates no capabilities and which no other client speaks — the same shape the diagram was in. The server now serves it as a real workspace/executeCommand returning the directory it wrote, and the action opens what is in it.
  • Live templates: fifteen — declarations (def, pdef, mod, enum, struct, trait, instance, eff, test) and expression scaffolding (region, try, run, foreach, rules, query). Deliberately not anything the server derives from a symbol: its two hand-written snippets are main and a default effect handler, and MagicMatchCompleter expands x.match into one case per constructor of x's enum, which no template could do. Every expansion is parsed by this plugin's own parser in FlixLiveTemplateSyntaxTest rather than reviewed — which caught the def template expanding to def f(): Unit =, leaving the buffer unparseable until the body was typed, and the language server reporting that as errors.
  • Settings: Settings > Languages & Frameworks > Flix — extra JVM arguments and extra Flix arguments, matching the VS Code extension's flix.extraJvmArgs and flix.extraFlixArgs. They reach the language server and every task, in the two positions the compiler's argument parser requires. Deliberately not the run/debug configuration, which has its own fields: a debug launch already passes --Xdebug, and a second occurrence is rejected outright as Unknown option --Xdebug.
  • Code lenses: both of them. flix.runMain above an entry point is live-verified and its symbol argument is honoured; flix.cmdTests above a test runs the test task. The second one used to fail with "Missing 'flix.cmdTests' command" because LSP4IJ resolves a server-defined command through ActionManager and only the first had an action — FlixAssembledPluginTest now pins both.
  • Gutter run arrow beside def main: anchored on the declaration's name leaf, delegating to the platform's generic ExecutorAction.
  • Split Mode: live-verified as an actual frontend+backend process pair (./gradlew runIdeSplitMode), with everything registered backend-side and the frontend a thin client. One real bug found getting here: an XML comment containing -- made the entire plugin fail to load on both sides, while verifyPluginProjectConfiguration and buildPlugin both passed. verifyPluginStructure does detect it, but does not fail the build over it -- its output has to be read, not just its exit code.

In an IDE without the Java plugin

Language support works: syntax, parsing, folding, the LSP features, the gutter marker. Only the debugger module needs the Java plugin, and it is declared optional, so an IDE without one skips it and loads the rest rather than failing. Debugging is the single feature that is unavailable there — not the plugin.

Not yet verified -- see verification coverage for the full matrix, which is explicit about what has been measured and what has not. The two that matter most: the native run configuration has never been exercised in a live session, and breakpoints on some Flix lines do not bind.

Debugging

Press Debug on a .flix file, or use the gutter arrow beside def main. There is no setup step: the Flix run configuration is created from the declaration under the caret.

What happens is deliberately unremarkable. The configuration launches the Flix compiler with run --Xdebug --yes and a JDWP agent on a free port, and IntelliJ's own Java debugger attaches to it. This plugin speaks no debug protocol; it decides which port to listen on and hands that to the platform.

--Xdebug is not optional, and is not only a JDWP switch. The compiler emits line numbers for let, calls, if and statement sequences only under it, and it turns the optimizer off entirely. Without it most statements have no breakpointable line at all, and a breakpoint on one can never verify no matter what the IDE does.

Turning the optimizer off is what keeps small functions breakpointable. A function whose body is folded into its caller gets no class of its own, and its line does not survive at the call site either — the inlined body starts at the call site's own bytecode offset, and only one line entry per offset reaches the class file. The line then exists nowhere in the program, which is the fate of every single-expression helper: pub def maxDemo(): Int32 \ IO = Math.max(10, 20) could never take a breakpoint while its multi-line neighbours could. The cost is that a debug session runs unoptimized, which is the trade every other toolchain makes.

Which compiler is used

In order: FLIX_JAR in the project's .envrc, then the $FLIX_JAR environment variable, then the compiler a flixw wrapper has pinned, then flix.jar in the project root. Every process the plugin starts -- the language server, a flix subcommand and the debuggee -- resolves it the same way, so a debug session cannot run a different compiler than the editor was analysed with.

The .envrc is parsed, never executed. direnv requires an explicit direnv allow before it will evaluate one, and opening a project in an editor is not that consent; the understood subset is export NAME=VALUE, and a value produced by a command substitution or guarded by a conditional is simply not seen. A flixw wrapper is read the same way -- its .flixw/lock.toml and cache layout, not ./flixw info -- so cloning a repository never runs a script it ships.

A wrapper also pins a JDK, and the plugin honours FLIX_JAVA_HOME and the JDK flixw installed before falling back to java on PATH. It deliberately does not reproduce flixw's full search: one of flixw's steps is "the JVM flixw is running on", and the JVM the IDE runs on is not the terminal's.

The build matters, not just the flag. Four fixes in wstein/flix-fork are load-bearing for debugging, and a jar predating any of them behaves as though the plugin is at fault:

Fix Without it
One LineNumberTable entry per bytecode offset A line sharing a bytecode offset with another cannot take a breakpoint. Its neighbours can, and javap still shows the line, so nothing looks wrong.
The declaration line yields its offset to the first statement The first statement of every function has no line entry at all, so a breakpoint on it never binds — while the def line above it and every later line do.
The optimizer is off under --Xdebug A single-expression function is folded into its caller, gets no class of its own, and its line survives nowhere — so pub def maxDemo(): Int32 \ IO = Math.max(10, 20) can never take a breakpoint.
Workspace jars loaded from the folder URI The language server resolves no [jar-dependencies], so every Java import reports "Undefined Java class" while the compiler builds the same project without complaint.

If breakpoints on some lines refuse to bind while adjacent ones work, rebuild the fork before looking anywhere else.

When a breakpoint does not bind

Check the class before suspecting the plugin:

javap -l -p 'build/class/Def$yourFunction.class' | grep -A20 LineNumberTable

A line absent from the table is a compiler-invocation problem, not an IDE one. If the line is there, turn on the plugin's own logging -- sandbox IDEs launched by runIde/runIdeSplitMode already have it enabled -- and read which step declined:

tail -f .intellijPlatform/sandbox/*/IU-*/system*/log/idea.log | grep dev.wstein

The gate runbook has a table mapping each log line to its cause.

Relationship to flix-lab

flix-lab is the companion project: a Flix workspace with VS Code tooling, and the fixture this plugin is developed against. It carries a Greeter in Java, Kotlin, Scala, Groovy and JRuby, all called from Main.flix, which is what makes mixed-language debugging testable.

The two projects no longer share code. They did: a FlixDebugAdapter.java DAP↔JDI bridge was vendored here and kept in sync by a script. That path was retired once IntelliJ's own JVM debugger proved out (ADR 0002), so the adapter now lives only in flix-lab, serving its VS Code client. What both projects still share is the compiler: the same flix-vendor-*.jar build of wstein/flix-fork.

Plugin structure

This repository implements a modular IntelliJ Platform plugin using content modules:

.
├── .github/                GitHub Workflows, issue templates, and Dependabot configuration
├── .qodana/profiles/       Qodana plugin inspections profile
├── .run/                   Predefined Run/Debug Configurations
├── language/               Language module -- Flix Language, FileType, parser, PSI, editor support
│   ├── build.gradle.kts    Grammar-Kit: generates the lexer/parser/PSI from src/main/grammar
│   └── src/
│       ├── main/
│       │   ├── grammar/    Flix.bnf, _Flix.flex, Flix.tokens.txt (adopted; see NOTICE)
│       │   ├── kotlin/org/flixlang/intellij/   language, editor, highlighting, gutter marker
│       │   └── resources/flix.jetbrains.plugin.language.xml
│       └── test/kotlin/    parser corpus gate, parsing, recovery, folding, gutter anchoring
├── debugger/               Debugger module -- Flix source positions for IntelliJ's JVM debugger
│   ├── build.gradle.kts    depends on the Java plugin; optional, so non-Java IDEs still load
│   └── src/
│       ├── main/kotlin/dev/wstein/flixplugin/
│       │   ├── debugger/   PositionManager, stepping policy, source lookup
│       │   └── run/        the Flix run/debug configuration and its context producer
│       └── test/kotlin/    JDI-stub tests for the dual-mode SMAP mapping rules
├── backend/                Backend module -- LSP4IJ server registration, flix.runMain, gutter marker
│   ├── build.gradle.kts    LSP4IJ dependency
│   └── src/
│       ├── main/
│       │   ├── java/dev/wstein/flixplugin/   Flix*.java (LSP factory, run action, jar resolution)
│       │   └── resources/flix.jetbrains.plugin.backend.xml  module descriptor
│       └── test/java/dev/wstein/flixplugin/  FlixForkTest
├── frontend/                Frontend module -- placeholder, no genuinely frontend-only UI yet
├── shared/                  Shared module -- compiler-jar resolution and launch command
├── src/
│   ├── main/resources/META-INF/plugin.xml   Root descriptor, declares the content modules
│   └── test/kotlin/        FlixPluginDescriptorTest -- registration-wiring invariants
├── buildSrc/               Build logic -- the integration-glue contract checker
├── flix-integration.yaml   Cross-module wiring contract; checked by `./gradlew checkIntegrationGlue`
├── build.gradle.kts        Root build -- assembles the final plugin, splitMode = true
├── gradle.properties
└── settings.gradle.kts

Module dependency syntax

Content module descriptors (flix.jetbrains.plugin.backend.xml etc.) use <dependencies><plugin id="..."/></dependencies> for external-plugin dependencies -- the classic <depends> tag from the root plugin.xml is explicitly disallowed inside a module descriptor (confirmed against the Modular Plugins documentation).

Build

./gradlew buildPlugin

Produces build/distributions/flix.jetbrains.plugin-<version>.zip.

Test

./gradlew test

FlixForkTest uses HeavyPlatformTestCase (a project backed by real files on disk), not the lighter BasePlatformTestCase (an in-memory VFS project) -- FlixFork.resolveJar does plain java.io/java.nio.file calls against project.getBasePath(), which only resolve against a real directory.

How the suite is split, and why

A content-module descriptor is inert on its own: the platform reads it only when the root plugin.xml names it in <content>. A module-local test fixture therefore never loads our <extensions>, and every LanguageBraceMatching.forLanguage-style lookup resolves null. Rather than assert wiring through a fixture that cannot represent it, the suite splits along that line:

  • Behaviour is asserted against the implementations directly, on real parsed PSI -- folding regions, brace pairs, commenter prefixes, gutter anchoring, incremental reparse.
  • Wiring is asserted by FlixPluginDescriptorTest against the descriptors themselves -- every content module declared, every extension naming a class that exists and implements its extension point's interface, exactly one file type claiming *.flix, and every descriptor well-formed.

Each failure then names one cause instead of two. The well-formedness check earns its place: -- inside an XML comment is invalid, is not caught by verifyPluginProjectConfiguration or buildPlugin, and makes the entire plugin fail to load with only "contains invalid plugin descriptor" to go on.

Parser corpus gate

FlixCorpusTest parses every .flix file the Flix compiler accepts -- 243 under main/src/library and 185 under examples -- and requires all of them to parse cleanly, losslessly and without crashing. It needs a Flix checkout, and skips when there is none, so CI without one still passes:

./gradlew test -PflixCorpusDir=/path/to/flix

FLIX_DIR and -DflixCorpusDir work too, and ~/github.com/flix/flix is tried by default. See the evaluation for what it measured and the one file it excludes.

Predefined Run/Debug configurations

Configuration name Description
Run IDE with Plugin (Frontend) Runs :runIdeFrontend. Use the Debug icon for plugin debugging.
Run IDE with Plugin (Backend) Runs :runIdeBackend. Use the Debug icon for plugin debugging.
Run IDE with Plugin (Split Mode) Runs both simultaneously to launch the plugin in split mode.

Install (sideload, not Marketplace-listed)

No JetBrains Marketplace listing currently -- consistent with flix-lab's VS Code extension being "private": true / sideloaded. Install the built zip manually via Settings/Preferences → Plugins → gear icon → Install Plugin from Disk...

You'll also need LSP4IJ installed from the Marketplace (it's a <plugin> dependency of the backend module; IntelliJ should prompt for it).

GitHub Actions / Qodana / Dependabot

Generator-provided scaffolding, unmodified: Build and Release workflows, issue templates, Dependabot config, and a Qodana inspections profile (.qodana/profiles/plugin.yaml, run locally via ./gradlew qodanaScan, requires Docker). None of these have been exercised yet (no CI run, no Qodana scan) -- they're present and should work per the generator's defaults, but that's unverified.

Direction: one language owner, native JVM debugging

Two architecture decisions shape the plugin, both recorded in docs/adr/:

  • ADR 0001 -- one Language("Flix"), adopted from intellij-flix rather than reimplemented. Two language owners means two parsers and two file types, and which one wins depends on load order.
  • ADR 0002 -- IntelliJ's own Java debugger is the sole JDWP owner. Two debuggers cannot share one debuggee: they compete for suspension, breakpoints and lifecycle.

Status. Both are implemented. The native debugger gate is Green: Flix and Java breakpoints in one session, stepping in both directions, mixed stack navigation, Java evaluation, and Flix-aware Step Over, with one JDWP owner throughout. The DAP path has been removed. What remains is verification rather than construction -- see verification coverage, which is explicit about which rows have been measured and which have not.

The property this buys, which a Flix-only debug adapter could not: a Flix frame can step into Java, Kotlin or Scala and back, with each language's own plugin owning its breakpoints, source positions and evaluator inside the same debug process.

Known gaps

  • The native run/debug configuration has never been exercised in a live session. It was built after the debugger gate, which ran entirely through a hand-made Remote JVM Debug configuration. With the DAP path removed there is no fallback if it misbehaves. Two blocking defects in it were found by review and fixed (GenericDebuggerRunner requires ModuleRunProfile on the configuration and RemoteConnectionCreator on the state); neither fix is confirmed live.
  • Breakpoints on some Flix lines do not bind. In flix-lab's Main.flix, lines calling into Kotlin/Scala/Groovy/JRuby do not bind while adjacent lines do. Artifact inspection has ruled out bytecode presence, class coverage, SMAP mapping and reachability -- the failing and working lines are indistinguishable in the class files. Tracked as row 9 of the verification coverage.
  • Flix values in CPS frames are not presented. A Clo$ continuation keeps its state in fields (l0..l8, pc) rather than locals, because the frame must survive suspension and resumption, so the variables view is empty for those frames. The values are present and reachable; reading them needs Flix-aware renderers, which the plan places after this milestone. Direct Def$ frames show variables normally.
  • verifyPlugin has never been run. The task exists; nothing invokes it.
  • Exception breakpoints, JDK/library source attachment, class redefinition and stale-cache invalidation have no coverage -- and no known failure either.
  • Kotlin, Scala and Groovy interop is unmeasured. flix-lab now carries a Greeter in five languages, which is the fixture for it, but no session has exercised them. Kotlin coroutine debugging is explicitly not claimed: it needs the Kotlin debugger's agent injected into an externally launched JVM, which has not been proven.

License

Apache License 2.0 -- see LICENSE.

NOTICE records the provenance of every derived component: the imported intellij-flix revision, the upstream Flix revision the grammar and token inventory are derived from (pinned as flixCorpusCommit in gradle.properties), and the Flix revision the grammar derives from.

Useful links

About

IntelliJ Platform plugin for the Flix programming language: syntax, LSP-backed editing, and native JVM debugging

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages