Code Completion Redesign Plan
Problem Statement
Current Architecture
The code completion pipeline for semantic completions (after .) is:
Broken MVEL text
→ ANTLR4 Parser (produces ErrorNodes for incomplete code)
→ TolerantMvel3ToJavaParserVisitor (patches ErrorNodes into JavaParser AST)
→ JavaSymbolSolver (resolves type of scope expression)
→ Completion items (fields, methods, properties)
TolerantMvel3ToJavaParserVisitor (312 lines) extends Mvel3ToJavaParserVisitor and overrides
three methods (visitBlock, visitBlockStatement, visitMemberReferenceExpression) to handle
incomplete expressions by detecting ANTLR ErrorNodes, reconstructing expressions from raw tokens,
inserting __COMPLETION_FIELD__ sentinel markers, and merging fragmented block statements.
Why It Is Fragile
-
Two overlapping recovery paths that fire unpredictably:
visitMemberReferenceExpression handles the case where ANTLR successfully creates a
MemberReferenceExpressionContext with a trailing dot (detected by hasTrailingDot()).
visitBlockStatement + buildIncompleteExpression handles the case where ANTLR failed
to produce any rule context and created raw ErrorNode children instead.
- Which path fires depends on ANTLR's error recovery heuristics, which are grammar-version-dependent
and vary with surrounding code. The same input can trigger different paths.
-
buildIncompleteExpression only understands identifier.identifier.identifier:
- Creates
NameExpr and FieldAccessExpr only — no parentheses, no arguments, no method calls.
- Cannot handle method chains:
list.stream().filter(x -> x > 5).
- Cannot handle MVEL null-safe access:
person!.address!. (EXCL_DOT is unrecognized).
-
Every new syntax construct before a trailing dot needs new tolerant handling:
- MVEL inline cast (
l#ArrayList#.), null-safe (person!.), method reference (Foo::), etc.
- Each requires case-by-case code in the tolerant visitor.
-
Pre-existing flaw in tokenIdJPNodeMap lookup (in Mvel3CompletionHelper, lines 191-213):
int scopeTokenIndex = previousTokenIndex - 1; // token before the DOT
Expression scopeNode = (Expression) tokenIdJPNodeMap.get(scopeTokenIndex);
This assumes the token immediately before . is mapped. But for list.stream()., the token
before . is ), and tokenIdJPNodeMap typically maps only identifiers — so scopeNode
is null for method chains. This must be fixed regardless of approach.
Considered Approaches
A. Text Pre-Processing / Placeholder Injection (Simple)
Inject __placeholder__ after trailing dot to produce syntactically valid text, then use the
normal Mvel3ToJavaParserVisitor.
- Works for:
person.address., l#ArrayList#., list.stream().map(...).
- Fails when: other broken code elsewhere in the file causes cascading ANTLR error recovery;
cursor is mid-code without semicolon injection; multiple incomplete expressions exist.
B. Scope Expression Extraction from Token Stream
Walk backward from cursor in the token stream, extract the scope expression text, parse it in
a controlled wrapper. Used by Eclipse JDT and kotlin-language-server.
- Immune to other broken code (isolated sub-parse).
- Requires a backward-walk parser for balanced parens, generics, MVEL tokens.
- Needs variable/type context injected into the wrapper.
C. Truncation + Placeholder (Kotlin-Style) — Recommended Primary Path
Truncate the file at the cursor position, inject placeholder + closing syntax, parse with
the normal visitor.
- Eliminates all broken-code-after-cursor problems (the most common scenario during editing).
- Combined with placeholder injection, covers the majority of real-world cases.
D. AST Caching with Localized Recovery
Cache the last successfully-parsed JavaParser AST. Use it as fallback when error recovery fails.
- Orthogonal to other approaches; improves robustness.
Chosen Architecture: Layered Hybrid
Layer 1 — Truncation + Placeholder (Primary Path)
Handle the common case: user is typing at or near the end of an expression.
Steps in Mvel3CompletionHelper.createSemanticCompletions():
- Truncate the input text at the cursor position (discard everything after the cursor line).
- Scan the trailing characters of the truncated text to detect the completion trigger pattern
(e.g., trailing ., !., #Type#.).
- Inject a synthetic identifier (
__completion__) after the trigger:
- Trailing
. → append __completion__
- Trailing
!. → append __completion__
- Trailing
#. (end of inline cast) → append __completion__
- Inject a semicolon if in statement context (not inside parentheses or brackets).
- Close open braces by counting unmatched
{ in the truncated text and appending matching }.
- Parse the patched text with the normal
Mvel3ToJavaParserVisitor (not tolerant).
- Walk the resulting JavaParser AST to find the
__completion__ node (a NameExpr or
FieldAccessExpr with that name). Navigate to its parent to get the scope expression.
- Resolve the scope type via
JavaSymbolSolver and produce completion items.
Why this works for the main cases:
| Input |
After truncation + injection |
Parses as |
person.address. |
person.address.__completion__; + closing braces |
Valid statement |
l#ArrayList#. |
l#ArrayList#.__completion__; + closing braces |
Valid inline cast + member access |
list.stream(). |
list.stream().__completion__; + closing braces |
Valid method chain + member access |
person!. |
person!.__completion__; + closing braces |
Valid null-safe + member access |
foo(bar.) (mid-expression) |
foo(bar.__completion__ + close parens/braces |
Valid nested expression |
Layer 2 — AST Walk Instead of tokenIdJPNodeMap
Replace the fragile token-index lookup:
// BEFORE (fragile):
int scopeTokenIndex = previousTokenIndex - 1;
Expression scopeNode = (Expression) tokenIdJPNodeMap.get(scopeTokenIndex);
// AFTER (robust):
// Walk the JavaParser AST to find the __completion__ sentinel node.
// The scope is the parent expression of that node.
Expression scopeNode = findCompletionScope(compilationUnit, "__completion__");
Implement findCompletionScope() as a JavaParser VoidVisitorAdapter that:
- Visits all
FieldAccessExpr nodes looking for name __completion__
- Visits all
NameExpr nodes looking for name __completion__
- Returns the scope (parent expression of the found node)
This fixes method-chain completion because the AST naturally represents list.stream() as a
MethodCallExpr, which is the correct scope node for type resolution.
Layer 3 — Fallback to ANTLR4-C3 Syntactic Completion
If the parse fails (broken code before the cursor that cascading error recovery cannot handle),
fall back to keyword/type suggestions from CodeCompletionCore, which already works today
(the getCompletionItemsFromTokens path).
This graceful degradation means the user always gets some completion — syntactic keywords at
minimum, semantic members when the parse succeeds.
Layer 4 (Future) — AST Caching
Cache the last successfully-parsed CompilationUnit (JavaParser AST). On each completion request:
- Attempt the truncation + placeholder parse (Layer 1).
- If it produces a valid AST, use it and update the cache.
- If it fails, use the cached AST for type resolution where possible.
This is an optimization for later — not required for the initial redesign.
What Gets Deleted
TolerantMvel3ToJavaParserVisitor.java (312 lines) — entirely removed from mvel repo.
tokenIdJPNodeMap lookup in createSemanticCompletions() — replaced by AST walk.
- The
__COMPLETION_FIELD__ sentinel concept moves from the visitor layer to the text
pre-processing layer in Mvel3CompletionHelper.
What Gets Added / Modified
Mvel3CompletionHelper.java (modified)
-
New private method: prepareTextForCompletion(String text, Position caretPosition)
- Truncates text at cursor line
- Detects completion trigger (
., !., #.)
- Injects
__completion__ identifier
- Injects semicolon if in statement context
- Closes open braces
- Returns the patched text
-
New private method: findCompletionScope(CompilationUnit cu, String sentinel)
- Walks JavaParser AST to find the sentinel node
- Returns the scope expression (parent of sentinel)
-
Modified createSemanticCompletions()
- Uses
prepareTextForCompletion() instead of TolerantMvel3ToJavaParserVisitor
- Uses
findCompletionScope() instead of tokenIdJPNodeMap lookup
- Uses the normal
Mvel3ToJavaParserVisitor for AST conversion
Estimated Complexity
| Component |
Lines (approx) |
prepareTextForCompletion() |
~40-60 |
findCompletionScope() |
~20-30 |
Modified createSemanticCompletions() |
~30 (simplified from current) |
| Net change |
~100 lines added, ~312 lines deleted |
Test Plan
Existing Tests (must continue to pass)
Mvel3CompletionHelperTest — complete/valid code completions
Mvel3CompletionHelperIncompleteCodeTest — the three existing incomplete-code tests:
emptyInput() — empty document
testInlineCast() — l#ArrayList#. completion
incompleteClass_PropertyAccessor() — p.address. completion
New Tests to Add
| Test |
Input |
Expected |
| Method chain completion |
list.stream(). |
filter, map, forEach, ... |
| Nested method call |
foo(bar.) where bar is typed |
members of bar's type |
| Null-safe completion |
person!. |
members of Person |
| Broken code before cursor |
int x = ;\nperson.address. |
members of Address (or graceful fallback) |
| Cursor mid-file |
person. followed by int y = 42; |
members of Person |
| Multiple dots in chain |
a.b.c. |
members of c's type |
| After method with args |
list.subList(0, 5). |
members of List |
Implementation Order
- Implement
prepareTextForCompletion() with unit tests for the text transformation itself
- Implement
findCompletionScope() AST walker
- Rewire
createSemanticCompletions() to use the new methods with normal Mvel3ToJavaParserVisitor
- Verify all existing tests pass
- Add new tests (method chains, null-safe, mid-file cursor, etc.)
- Delete
TolerantMvel3ToJavaParserVisitor.java from the mvel repo
- Remove
tokenIdJPNodeMap usage from completion code (the field remains in the base visitor
for other use cases in the mvel compiler)
Risks
- Brace counting for closing open braces may be tricky with string literals and comments
that contain { characters. Use a simple state machine (track whether inside string/comment)
rather than naive character counting.
- ANTLR error recovery for code before the cursor remains unpredictable. The fallback to
ANTLR4-C3 syntactic completion mitigates this.
JavaSymbolSolver resolution failures for complex generic types or MVEL-specific AST nodes
may require targeted fixes. Handle with try-catch and graceful degradation.
Code Completion Redesign Plan
Problem Statement
Current Architecture
The code completion pipeline for semantic completions (after
.) is:TolerantMvel3ToJavaParserVisitor(312 lines) extendsMvel3ToJavaParserVisitorand overridesthree methods (
visitBlock,visitBlockStatement,visitMemberReferenceExpression) to handleincomplete expressions by detecting ANTLR
ErrorNodes, reconstructing expressions from raw tokens,inserting
__COMPLETION_FIELD__sentinel markers, and merging fragmented block statements.Why It Is Fragile
Two overlapping recovery paths that fire unpredictably:
visitMemberReferenceExpressionhandles the case where ANTLR successfully creates aMemberReferenceExpressionContextwith a trailing dot (detected byhasTrailingDot()).visitBlockStatement+buildIncompleteExpressionhandles the case where ANTLR failedto produce any rule context and created raw
ErrorNodechildren instead.and vary with surrounding code. The same input can trigger different paths.
buildIncompleteExpressiononly understandsidentifier.identifier.identifier:NameExprandFieldAccessExpronly — no parentheses, no arguments, no method calls.list.stream().filter(x -> x > 5).person!.address!.(EXCL_DOTis unrecognized).Every new syntax construct before a trailing dot needs new tolerant handling:
l#ArrayList#.), null-safe (person!.), method reference (Foo::), etc.Pre-existing flaw in
tokenIdJPNodeMaplookup (inMvel3CompletionHelper, lines 191-213):This assumes the token immediately before
.is mapped. But forlist.stream()., the tokenbefore
.is), andtokenIdJPNodeMaptypically maps only identifiers — soscopeNodeis
nullfor method chains. This must be fixed regardless of approach.Considered Approaches
A. Text Pre-Processing / Placeholder Injection (Simple)
Inject
__placeholder__after trailing dot to produce syntactically valid text, then use thenormal
Mvel3ToJavaParserVisitor.person.address.,l#ArrayList#.,list.stream().map(...).cursor is mid-code without semicolon injection; multiple incomplete expressions exist.
B. Scope Expression Extraction from Token Stream
Walk backward from cursor in the token stream, extract the scope expression text, parse it in
a controlled wrapper. Used by Eclipse JDT and kotlin-language-server.
C. Truncation + Placeholder (Kotlin-Style) — Recommended Primary Path
Truncate the file at the cursor position, inject placeholder + closing syntax, parse with
the normal visitor.
D. AST Caching with Localized Recovery
Cache the last successfully-parsed JavaParser AST. Use it as fallback when error recovery fails.
Chosen Architecture: Layered Hybrid
Layer 1 — Truncation + Placeholder (Primary Path)
Handle the common case: user is typing at or near the end of an expression.
Steps in
Mvel3CompletionHelper.createSemanticCompletions():(e.g., trailing
.,!.,#Type#.).__completion__) after the trigger:.→ append__completion__!.→ append__completion__#.(end of inline cast) → append__completion__{in the truncated text and appending matching}.Mvel3ToJavaParserVisitor(not tolerant).__completion__node (aNameExprorFieldAccessExprwith that name). Navigate to its parent to get the scope expression.JavaSymbolSolverand produce completion items.Why this works for the main cases:
person.address.person.address.__completion__;+ closing bracesl#ArrayList#.l#ArrayList#.__completion__;+ closing braceslist.stream().list.stream().__completion__;+ closing bracesperson!.person!.__completion__;+ closing bracesfoo(bar.)(mid-expression)foo(bar.__completion__+ close parens/bracesLayer 2 — AST Walk Instead of
tokenIdJPNodeMapReplace the fragile token-index lookup:
Implement
findCompletionScope()as a JavaParserVoidVisitorAdapterthat:FieldAccessExprnodes looking for name__completion__NameExprnodes looking for name__completion__This fixes method-chain completion because the AST naturally represents
list.stream()as aMethodCallExpr, which is the correct scope node for type resolution.Layer 3 — Fallback to ANTLR4-C3 Syntactic Completion
If the parse fails (broken code before the cursor that cascading error recovery cannot handle),
fall back to keyword/type suggestions from
CodeCompletionCore, which already works today(the
getCompletionItemsFromTokenspath).This graceful degradation means the user always gets some completion — syntactic keywords at
minimum, semantic members when the parse succeeds.
Layer 4 (Future) — AST Caching
Cache the last successfully-parsed
CompilationUnit(JavaParser AST). On each completion request:This is an optimization for later — not required for the initial redesign.
What Gets Deleted
TolerantMvel3ToJavaParserVisitor.java(312 lines) — entirely removed frommvelrepo.tokenIdJPNodeMaplookup increateSemanticCompletions()— replaced by AST walk.__COMPLETION_FIELD__sentinel concept moves from the visitor layer to the textpre-processing layer in
Mvel3CompletionHelper.What Gets Added / Modified
Mvel3CompletionHelper.java(modified)New private method:
prepareTextForCompletion(String text, Position caretPosition).,!.,#.)__completion__identifierNew private method:
findCompletionScope(CompilationUnit cu, String sentinel)Modified
createSemanticCompletions()prepareTextForCompletion()instead ofTolerantMvel3ToJavaParserVisitorfindCompletionScope()instead oftokenIdJPNodeMaplookupMvel3ToJavaParserVisitorfor AST conversionEstimated Complexity
prepareTextForCompletion()findCompletionScope()createSemanticCompletions()Test Plan
Existing Tests (must continue to pass)
Mvel3CompletionHelperTest— complete/valid code completionsMvel3CompletionHelperIncompleteCodeTest— the three existing incomplete-code tests:emptyInput()— empty documenttestInlineCast()—l#ArrayList#.completionincompleteClass_PropertyAccessor()—p.address.completionNew Tests to Add
list.stream().filter,map,forEach, ...foo(bar.)wherebaris typedperson!.int x = ;\nperson.address.person.followed byint y = 42;a.b.c.list.subList(0, 5).Implementation Order
prepareTextForCompletion()with unit tests for the text transformation itselffindCompletionScope()AST walkercreateSemanticCompletions()to use the new methods with normalMvel3ToJavaParserVisitorTolerantMvel3ToJavaParserVisitor.javafrom the mvel repotokenIdJPNodeMapusage from completion code (the field remains in the base visitorfor other use cases in the mvel compiler)
Risks
that contain
{characters. Use a simple state machine (track whether inside string/comment)rather than naive character counting.
ANTLR4-C3 syntactic completion mitigates this.
JavaSymbolSolverresolution failures for complex generic types or MVEL-specific AST nodesmay require targeted fixes. Handle with try-catch and graceful degradation.