Context & Goals
The current Indexer architecture in kotlin-lsp-rs is incredibly fast, memory-safe, and highly concurrent. However, to elevate this LSP into a robust IDE-grade developer experience, we need to bridge the gap between fast autocomplete and true Semantic Analysis & Static Type Safety.
I am planning to implement a comprehensive suite of features over the next few days to introduce full diagnostic reporting (red squiggly lines), advanced cross-platform KMP navigation, and smart casting.
Below is the detailed architectural roadmap of what I am currently working on.
🗺️ Engineering Roadmap & Feature Specs
1. Cross-Platform KMP Navigation (expect / actual CodeLens) ✅ COMPLETED
- The Problem: Navigating Kotlin Multiplatform (KMP) codebases between
commonMain definitions and native target implementations (androidMain, iosMain) is friction-heavy.
- The Solution: Implement an LSP
textDocument/codeLens provider.
- Rust Implementation: Our
Indexer already parses fully-qualified names globally. We will traverse the file's AST looking for symbols marked with expect or actual. The CodeLens provider will query the qualified map in the Indexer to resolve cross-module links and overlay virtual clickable text (└── ◆ iOS Actual | ▣ Android Actual). Clicking it invokes textDocument/definition via a resolved Location link.
- Extras:
- Neovim/Helix support via
window/showDocument fallback in execute_command
- Dynamic modifier verification — live-tree parse confirms counterpart carries the expected modifier
- Missing-actual diagnostics — warns when a platform source set lacks an
actual implementation for a declared expect
- One-click code action — creates missing
actual files with TODO body and post-creation navigation
- Platform groups simplified to match real KMP default hierarchy (10 groups: common, android, apple, ios, jvm, js, native, macos, linux, wasm)
2. Dead Code Elimination (Unused Symbols Diagnostic)
- The Problem: Dead code and stale imports reduce project maintainability and performance.
- The Solution: Emit
DiagnosticSeverity::HINT on unused variables, functions, and imports so editors can grey them out, coupled with a CodeAction to remove them.
- Rust Implementation:
During the AST traversal pass of a single file, we will collect all internal declarations into a HashSet. As the file walker parses the expression blocks, any identifier hit will be removed from the HashSet. At the end of the document pass, the remaining elements in the set represent dead code, triggering a hint diagnostic.
3. Smart Casts & Scope-Aware Type Overrides
- The Problem: Kotlin relies heavily on smart casts (e.g.,
if (obj is String) { ... }). Outside that block, using it as a subclass should throw a type error, while inside, its type must be dynamically upgraded.
- The Solution: Introduce a lightweight scope-tracking mechanism or a micro-Control Flow Graph (CFG) inside our diagnostic pass.
- Rust Implementation:
When evaluating an if_expression containing an is_expression condition, the local semantic resolver will inject a temporary type override inside that specific block scope. Upon reaching the end node (}) of the block, the type variable falls back to its original supertype (Any). Accessing subclass methods outside the scope will trigger an unresolved method diagnostic.
4. Unresolved Supertypes & Inheritance Restrictions
- The Problem: In Kotlin, classes are
final by default. Inheriting from a closed class without an open, abstract, or interface modifier is a compilation error.
- The Solution: Emit a compiler error diagnostic when a class attempts to extend a non-open parent.
- Rust Implementation:
When the parser encounters a class inheritance node (e.g., class MyIntent : Intent()), the engine will query the Indexer for the Intent superclass declaration. We will inspect its cached modifiers. If it lacks open/abstract/interface, we will emit: This type is final, so it cannot be inherited from.
📈 Current Progress & PR Tracking
To keep everything transparent and prevent any duplication of work, here is the current status of my open pull requests and active contributions:
- [OPEN PR] Mutability Enforcement: * PR:
#137 feat: add val-reassignment diagnostics via live-CST walk
- Spec: Flags
assignment_expression mutations where the left-hand side identifier resolves to a read-only val property (is_writable == false).
- [OPEN PR] Code Generation & Refactoring: * PR:
#134 feat:Generate code actions: getters, setters, and override methods
- Spec: Introduces Quick Fix/Code Actions to automatically generate standard Boilerplate Getters, Setters, and unimplemented/override methods for class properties.
- [OPEN PR] Supertype Fallbacks: * PR:
#132 feat:(generate_constructor): add resolver fallback chain for unindexed supertypes
- Spec: Resolves empty constructor/initialization chains safely even when dealing with unindexed external supertypes.
- [COMPLETED]
goToDefinition Scope Resolution Fix:
- Status: Resolved in
feat/kmp-codelens branch.
- Spec: Fixed an edge case where invoking
goToDefinition incorrectly jumped to a variable sharing the exact same name within the same file, even though it belonged to an entirely different, isolated scope.
- [NEW PR] KMP CodeLens + Diagnostics + Code Action: * Branch:
feat/kmp-codelens
- Status: CodeLens navigation for
expect/actual across all KMP source sets, missing-actual diagnostics, one-click file creation code action, Neovim support, platform groups matching real KMP hierarchy. All 1201 tests pass, clippy clean.
Next Steps & Feedback
I will be rolling out the implementations for Smart Casts and Unused Diagnostics incrementally over the next couple of days.
@Hessesian — Please let me know your thoughts on this roadmap or if you have specific architectural preferences before I submit the next batch of PRs. Let's make this LSP absolute lightning! ⚡
Context & Goals
The current
Indexerarchitecture inkotlin-lsp-rsis incredibly fast, memory-safe, and highly concurrent. However, to elevate this LSP into a robust IDE-grade developer experience, we need to bridge the gap between fast autocomplete and true Semantic Analysis & Static Type Safety.I am planning to implement a comprehensive suite of features over the next few days to introduce full diagnostic reporting (red squiggly lines), advanced cross-platform KMP navigation, and smart casting.
Below is the detailed architectural roadmap of what I am currently working on.
🗺️ Engineering Roadmap & Feature Specs
1. Cross-Platform KMP Navigation (
expect/actualCodeLens) ✅ COMPLETEDcommonMaindefinitions and native target implementations (androidMain,iosMain) is friction-heavy.textDocument/codeLensprovider.Indexeralready parses fully-qualified names globally. We will traverse the file's AST looking for symbols marked withexpectoractual. The CodeLens provider will query thequalifiedmap in theIndexerto resolve cross-module links and overlay virtual clickable text (└── ◆ iOS Actual | ▣ Android Actual). Clicking it invokestextDocument/definitionvia a resolvedLocationlink.window/showDocumentfallback inexecute_commandactualimplementation for a declaredexpectactualfiles with TODO body and post-creation navigation2. Dead Code Elimination (Unused Symbols Diagnostic)
DiagnosticSeverity::HINTon unused variables, functions, and imports so editors can grey them out, coupled with aCodeActionto remove them.During the AST traversal pass of a single file, we will collect all internal declarations into a
HashSet. As the file walker parses the expression blocks, any identifier hit will be removed from theHashSet. At the end of the document pass, the remaining elements in the set represent dead code, triggering a hint diagnostic.3. Smart Casts & Scope-Aware Type Overrides
if (obj is String) { ... }). Outside that block, using it as a subclass should throw a type error, while inside, its type must be dynamically upgraded.When evaluating an
if_expressioncontaining anis_expressioncondition, the local semantic resolver will inject a temporary type override inside that specific block scope. Upon reaching the end node (}) of the block, the type variable falls back to its original supertype (Any). Accessing subclass methods outside the scope will trigger an unresolved method diagnostic.4. Unresolved Supertypes & Inheritance Restrictions
finalby default. Inheriting from a closed class without anopen,abstract, orinterfacemodifier is a compilation error.When the parser encounters a class inheritance node (e.g.,
class MyIntent : Intent()), the engine will query theIndexerfor theIntentsuperclass declaration. We will inspect its cached modifiers. If it lacksopen/abstract/interface, we will emit:This type is final, so it cannot be inherited from.📈 Current Progress & PR Tracking
To keep everything transparent and prevent any duplication of work, here is the current status of my open pull requests and active contributions:
#137 feat: add val-reassignment diagnostics via live-CST walkassignment_expressionmutations where the left-hand side identifier resolves to a read-onlyvalproperty (is_writable == false).#134 feat:Generate code actions: getters, setters, and override methods#132 feat:(generate_constructor): add resolver fallback chain for unindexed supertypesgoToDefinitionScope Resolution Fix:feat/kmp-codelensbranch.goToDefinitionincorrectly jumped to a variable sharing the exact same name within the same file, even though it belonged to an entirely different, isolated scope.feat/kmp-codelensexpect/actualacross all KMP source sets, missing-actual diagnostics, one-click file creation code action, Neovim support, platform groups matching real KMP hierarchy. All 1201 tests pass, clippy clean.Next Steps & Feedback
I will be rolling out the implementations for Smart Casts and Unused Diagnostics incrementally over the next couple of days.
@Hessesian — Please let me know your thoughts on this roadmap or if you have specific architectural preferences before I submit the next batch of PRs. Let's make this LSP absolute lightning! ⚡