Skip to content

Add XTC Language Support: LSP server, IntelliJ plugin, and VS Code extension#377

Merged
lagergren merged 52 commits intomasterfrom
lagergren/init
Feb 4, 2026
Merged

Add XTC Language Support: LSP server, IntelliJ plugin, and VS Code extension#377
lagergren merged 52 commits intomasterfrom
lagergren/init

Conversation

@lagergren
Copy link
Contributor

@lagergren lagergren commented Jan 23, 2026

Summary

XTC language tooling infrastructure with IDE support and high-quality parsing.

Tree-sitter Grammar

  • 100% coverage on all 692 XTC test files with only 49 GLR conflicts
  • External scanner (Kotlin DSL → C) for template strings ($"text {expr}", $|multiline|) and TODO freeform text
  • Zig cross-compilation builds native libraries for all 5 platforms from any dev machine:
    • darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64
  • Pre-built libraries committed to source control (no Zig download in CI)
  • JVM integration via jtreesitter using Java's Foreign Function & Memory API

LSP Server

  • Pluggable adapter architecture: MockXtcCompilerAdapter (regex, for testing) → TreeSitterAdapter (syntax-level) → future CompilerAdapter (semantic)
  • Switch adapters via -Plsp.adapter=treesitter
  • Comprehensive logging with parse times, query execution, symbol resolution

IDE Plugins

  • IntelliJ: New Project wizard, run configurations (--module, --method, --args), TextMate highlighting, LSP via lsp4ij
  • VS Code: Extension skeleton with TextMate grammar

Other

  • xtc init command: New launcher command for initializing XTC projects with --dir option
  • DSL generators: TextMate, Vim, Emacs, Sublime Text, tree-sitter - all from single XtcLanguage.kt model
  • Gradle plugin: Added --module, --method, --args options to runXtc task

Build Infrastructure

  • includeBuildLang flag controls lang build inclusion (disabled by default for CI stability)
  • Configuration cache compatible throughout
  • ktlint added to lang Kotlin projects

Status: Alpha - This code is unsupported and under active development. APIs may change without notice.

Test plan

  • Tree-sitter grammar parses 100% of XTC corpus (692 files)
  • Native libraries build for all 5 platforms via Zig
  • Build works with includeBuildLang=false (master compatibility)
  • Configuration cache compatibility verified
  • runIntellijPlugin depends on publishLocal for proper sandbox IDE testing

@lagergren lagergren force-pushed the lagergren/init branch 2 times, most recently from 4f18ce7 to 2a95b68 Compare January 23, 2026 11:26
Copy link
Collaborator

@ggleyzer ggleyzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could move the "lang" module into its own repo (e.g. "xtclang/lsp")? It seems that would make everything a bit cleaner...

@lagergren lagergren force-pushed the lagergren/init branch 3 times, most recently from b6c9d0f to 58785c2 Compare January 24, 2026 12:50
@angelozerr
Copy link

It is super news for LSP4IJ !

When your plugin will be available please add it to the lsp4j readme https://github.com/redhat-developer/lsp4ij?tab=readme-ov-file#who-is-using-lsp4ij

And if you like LSP4IJ please add a review in Jetbrains marketplace to help us to promote LSP4IJ

Thanks!

@lagergren
Copy link
Contributor Author

I wonder if we could move the "lang" module into its own repo (e.g. "xtclang/lsp")? It seems that would make everything a bit cleaner...

It is a composite build. There is virtually nothing outside the lang repo that were are referring to in any other place, except for the version catalog. I also heavily depend on project independent build logic, the javatools, and various other things that I access from lang but not in the other way around. I see no "cleaner" way to do it that would not inline both new and modified gradle work. I also want to be able to do stuff line run intellij on the current code base. It certainly not in the way for anyone, and if you don't even want to resolve the build against it, you can, just like manualTests, replace the attach and include flags in gradle.properties to ignore the gradle project. So I would very much like to get these changes in ASAP, without having to risk the rather complicated dependencies towards the rest of the XDK for now. The build structure for lang was very hard to get working, and I would appreciate it if we could work more incrementally with that. I need this code base to continue, and I am heavily relying on our common build logic for version control against what the XDK is built on. As fast as a I understand it, this seems to be best practice.

@lagergren lagergren requested a review from ggleyzer January 26, 2026 14:27
Copy link
Collaborator

@cpurdy cpurdy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I like the changes to the README, particularly moving massive chunks into their own specific "read more" files. I was going to suggest something like this, so I'm glad to see you beat me to it.
  2. I like the changes to the .gitignore file.
  3. gradle.properties "TODO: LSP do not check in true/true here. Used for dev" -- can you verify that you actually want this in the commit?
  4. DockerTest.x -- was this change intentional? (Trailing whitespace only.) The only thing that worries me if it wasn't intentional is that you let the reviewer (me) find this.
  5. Initializer tool -- I have no issues with having support for tools, including Gradle/Maven, as long as developers using Ecstasy have zero required dependencies on those tools. My big concern here is that the Initializer stuff seems a bit brittle, but I guess we'll find out over time if that concern is real.
  6. javatools-utils/build.gradle.kts -- I think I found where that trailing whitespace in DockerTest.x came from!
  7. javatools-utils/README.md -- never mind, it probably came from here 🤣
  8. Lazy/Suppiers -- These look fine, but were unnecessary as part of this overall change. I understand the desire to "upgrade" things as you go, and it's a good desire. It's something that we also try to do consistently. But work on building consensus in advance, though, i.e. involve others in the process up front. In this case, you replaced thread safe (via idempotency) code with thread safe (via a different mechanism) code. The upside of the change is unlikely to be realized in this particular change set, but I can see opportunities for benefits down the road, which is exactly why getting more buy-in (and thus awareness) is such an important part of the process! I would like you to do a walk through of this functionality on a team call to get others up to speed!
  9. lang/README.md -- Very good overall. We try not to refer to "XTC" ... Ecstasy is the language name, and xtclang is also usable, referring to the "language project" I guess.
  10. vscode extension -- Hard for me to provide much feedback. I've read through everything, but this is not my area of expertise. Again, I worry about some brittleness, but I also accept that tool integration is likely where we will be forced to deal with brittleness no matter how clever we are.
  11. IntelliJ plugin -- Similar summary as for vscode, in that I am not an expert on the IDE integration, and I am worried about brittleness. But the README is well done and we're still in a learning phase, so I am comfortable with the content in that context.

Next up I plan to go through the LSP Server and DSL sub-projects. I've simply run out of hours today. Overall, this is a huge effort, and I am looking forward to seeing it in action!

@lagergren lagergren force-pushed the lagergren/init branch 3 times, most recently from ef29121 to 9b3c2c0 Compare January 27, 2026 08:41
lagergren and others added 11 commits January 27, 2026 09:42
Replace manual temp directory creation and cleanup with JUnit 5's
@tempdir annotation which guarantees cleanup even if tests fail.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move LSP server code to dedicated lang/lsp-server subproject
- Implement in-process StreamConnectionProvider using piped streams
- Fix LSP4J dependency conflict: use compileOnly since LSP4IJ provides
  LSP4J at runtime (avoids ClassCastException from classloader conflicts)
- Remove subprocess approach and hardcoded JAR paths
- Target JDK 21 for IntelliJ 2025.1 compatibility
- Add TextMateBundleProvider for dynamically generated grammar
- Remove System.exit() from server exit() to support in-process execution

The LSP server now runs in the same JVM as IntelliJ, communicating via
piped input/output streams. This eliminates subprocess management issues
and resolves the "Cannot start server (pid=null)" errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add InitializerOptions to LauncherOptions.java with CLI schema
- Refactor Initializer.java to use new Launcher<T> pattern
- Add CMD_INIT to Launcher.java COMMANDS map and launch() switch
- Update showHelp() to include init command
- Fix InitializerTest to use isError() instead of private field
- Add comprehensive XTC CLI documentation (doc/XTC_CLI.md)
- Link CLI docs from main README

The xtc init command now works like other commands (build, run, test):
  xtc init myapp                    # Create application
  xtc init mylib --type=library     # Create library
  xtc init myproj --multi-module    # Create multi-module project

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allows specifying the parent directory for project creation:
  xtc init myapp --dir=/path/to/parent

The project is created at <dir>/<name> instead of ./<name>.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These are generated files from the IntelliJ Platform Gradle Plugin
and should not be tracked.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Ensures the latest XTC Gradle plugin is published to local Maven before
the sandbox IDE starts. This fixes Run Configurations that use the
--module, --method, --args command-line options which older plugin
versions don't support.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Key changes:
- Remove safe_call_else_expression which was blocking member expression + call
- Fix x?.foo() to correctly parse as call_expression(member_expression)
- Support static @abstract const (annotations after static modifier)
- Add $._non_bi_type vs generic_type conflict

Coverage: 355/691 (51%). The x?.foo() fix is critical for correct parsing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
- Add support for empty array creation: new Type[]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added the following grammar features:
- Else expression: `expr?.method() : fallback` for short-circuit fallbacks
- Short-circuit postfix: `expr?` for null checking
- TODO expression: `TODO`, `TODO(msg)`, `TODO text` for placeholders
- Type patterns in case: `case List<String>:` for type matching

Coverage improved from 381/691 (55.1%) to 419/692 (60.5%) XTC files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… versioning

- Rename VSCodePackageGenerator to TextMateBundleManifestGenerator to reflect
  that it generates TextMate bundle manifests used by VS Code, IntelliJ, etc.
- Fix version handling: Gradle now passes project.version as system property
  with proper cache input declaration; generators fail if version not provided
- Derive language ID from scopeName for consistency (source.xtc -> xtc)
- Rename PLAN_OUT_OF_PROCESS_LSP.md to lsp-processes.md, update all references
- Update dsl/README.md to list actual existing files
- Sync generated-examples with current generators (was stale)
- Remove .claude/ from source control, update .gitignore
- Fix typo in grammar.js.template
- Fix ktlint formatting issues
- Add TODO to README about Ecstasy/XTC naming convention
- Add comment explaining tree-sitter workDir and harmless warnings
By default, ktlint only runs as part of the 'check' task, which means
development tasks like runIde, jar, and assemble skip ktlint entirely.
This caused CI failures when formatting violations were committed.

Fix by making compileKotlin depend on ktlintCheck in all lang subprojects.
Replace 620 lines across 3 files (JreProvisioner, FoojayClient,
PlatformDetector) with a single 200-line JreProvisioner that:
- Uses Foojay Disco API for JRE discovery
- Uses IntelliJ's built-in Decompressor.Tar/Zip for extraction
- Adds extensive logging for troubleshooting
- Update PLAN_TREE_SITTER.md and lsp-processes.md for Java 25 and JRE provisioning
- Add Platform.kt utility to consolidate platform detection in lsp-server
- Update TreeSitterAdapter MIN_JAVA_VERSION to 25
- Add detailed logging for definition/references lookups
- Remove unused gitCommitShort provider from tree-sitter build
- Add TARGET_VERSION constant back to JreProvisioner
- Forward lsp.logLevel from IntelliJ to LSP server process
- Update logback.xml to read lsp.logLevel property
- Accepts DEBUG, INFO, WARN, ERROR (case-insensitive)

Usage: ./gradlew :lang:intellij-plugin:runIde -Dlsp.logLevel=DEBUG
- Rewrite XtcNewProjectWizardStep with idiomatic Kotlin (runCatching, when, apply)
- Use Java record properties directly (result.success, result.message)
- Fix KDoc references to use literal values instead of unresolvable links
- Fix ktlint chain-method-continuation warning
JreProvisioner now uses a smarter resolution strategy:
1. Check IntelliJ's ProjectJdkTable for registered Java 25+ SDKs
2. Fall back to cached JRE in IDE's system directory
3. Download Temurin JRE 25 via Foojay API only if needed

Key changes:
- Add com.intellij.modules.java dependency for SDK APIs
- Use JavaSdk.getVMExecutablePath() instead of hardcoded paths
- Cache JRE in PathManager.getSystemPath() (IDE-managed location)
- Add comprehensive KDoc explaining the provisioning strategy
- Fix archive extraction to flatten nested directories
- Add failure marker to prevent infinite retry loops
- Make XtcIconProvider more idiomatic Kotlin (runCatching)
- Update plans to reflect new architecture
…ation cache in Gradle, but we can add some boiler plate to force intellij to read the Gradle toolchain configuration, so we get rid that annoying 'stuff was changed in .idea, and this is a JDK 1.6 project now - sorry'.
…sitter task always run. Now has proper imports, which should be fine both for build or CI
These integration tests spawn separate Gradle processes for each generated
project, downloading dependencies and the Gradle wrapper from scratch. With
an empty cache, this adds several minutes to the build.

Enable with: RUN_INTEGRATION_TESTS=true ./gradlew javatools:test
- Unify log level property to xtc.logLevel across all XTC tooling
- Add file-based logging to ~/.xtc/logs/lsp-server.log with rolling policy
- Log file path and PID in startup banner for easier debugging
- Add extension functions for formatting LSP Position/Range types
- Use import aliases to clean up verbose fully-qualified type names
- Move companion objects to top of classes (Kotlin idiomatic)
- Simplify hover info generation with Elvis operator and ?.let
- Add explicit classes dependency to fatJar for proper rebuilds
- Update lsp-processes.md documentation for xtc.logLevel
Add Lsp4jConversions.kt with extension functions for converting between
model types and LSP4J types:
- Location.toRange(), Location.toLsp(), Location.fromLsp()
- Diagnostic.Severity.toLsp(), Diagnostic.Severity.fromLsp()
- Diagnostic.toLsp(), Diagnostic.fromLsp()
- SymbolInfo.SymbolKind.toLsp()
- XtcCompilerAdapter inner types (HighlightKind, FoldingKind, etc.)

This removes ~100 lines of scattered inline conversion code from
XtcLanguageServer.kt and consolidates all LSP4J mappings in one place.

Other improvements:
- Add XtcLanguageConstants.kt for shared keyword/type data
- Add XtcCompilerAdapterStub for compiler adapter placeholder
- Add companion objects to Diagnostic.Severity and SymbolInfo.SymbolKind
- Clean up unused imports and type aliases
- Add tree-sitter feature matrix documentation
- Add AbstractXtcCompilerAdapter base class with shared:
  - Per-class logger (lazy initialized)
  - logPrefix property for consistent log formatting
  - getHoverInfo() default implementation
  - Location.contains() utility extension
  - Closeable with no-op close()

- Add XtcLanguageConstants utilities:
  - SymbolInfo.toHoverMarkdown() extension
  - keywordCompletions() and builtInTypeCompletions() helpers

- Simplify adapters:
  - MockXtcCompilerAdapter: use top-level camelCase regex vals, destructuring
  - TreeSitterAdapter: remove duplicate code, use shared utilities
  - XtcCompilerAdapterStub: minimal placeholder with @workinprogress

- Delete unused XtcCompilerAdapterFull interface (was never implemented)

- Add @workinprogress annotation in org.xvm.lsp.util

- Add quietMode (-q flag) to XtcRunConfiguration for less verbose Gradle output

- Add logging to XtcRunConfiguration

- Update documentation to reflect new adapter hierarchy

Net change: -600 lines (consolidation of duplicate code)
- Fix table formatting in functionality.md and tree-sitter README
- Update Lsp4jConversions.kt formatting
- Minor documentation improvements
Document what compiler phases are needed for each feature:
- XtcCompilerAdapterStub: All methods need Phase 4-5 integration
- TreeSitterAdapter: Note limitations of syntax-only approach
- Make SCANNER_DEBUG configurable via generate(debug: Boolean = false)
- CLI accepts --debug flag to enable scanner debug output
- Temporarily disable tree-sitter validation step in CI (if: false)
- Preserve original condition as comment for re-enabling later
@lagergren lagergren merged commit 35f417a into master Feb 4, 2026
17 checks passed
@lagergren lagergren deleted the lagergren/init branch February 4, 2026 21:01
lagergren added a commit that referenced this pull request Feb 7, 2026
Add remaining changes that were not included in the initial LSP server
PR: extended XtcLanguageServer implementation, additional unit tests,
and editor setup documentation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants