Skip to content

Commit c2cd5c8

Browse files
authored
Improve next element computation in completion (#1215)
1 parent 6f22f22 commit c2cd5c8

File tree

4 files changed

+56
-51
lines changed

4 files changed

+56
-51
lines changed

packages/langium/src/lsp/completion/completion-provider.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { LangiumCompletionParser } from '../../parser/langium-parser.js';
1010
import type { NameProvider } from '../../references/name-provider.js';
1111
import type { ScopeProvider } from '../../references/scope-provider.js';
1212
import type { LangiumServices } from '../../services.js';
13-
import type { AstNode, AstNodeDescription, CstNode, Reference, ReferenceInfo } from '../../syntax-tree.js';
13+
import type { AstNode, AstNodeDescription, AstReflection, CstNode, ReferenceInfo } from '../../syntax-tree.js';
1414
import type { MaybePromise } from '../../utils/promise-util.js';
1515
import type { LangiumDocument } from '../../workspace/documents.js';
1616
import type { NextFeature } from './follow-element-computation.js';
@@ -22,7 +22,7 @@ import type { IToken } from 'chevrotain';
2222
import { CompletionItemKind, CompletionList, Position } from 'vscode-languageserver';
2323
import * as ast from '../../grammar/generated/ast.js';
2424
import { getExplicitRuleType } from '../../grammar/internal-grammar-util.js';
25-
import { getContainerOfType } from '../../utils/ast-util.js';
25+
import { assignMandatoryAstProperties, getContainerOfType } from '../../utils/ast-util.js';
2626
import { findDeclarationNodeAtOffset, findLeafNodeAtOffset } from '../../utils/cst-util.js';
2727
import { getEntryRule } from '../../utils/grammar-util.js';
2828
import { stream } from '../../utils/stream.js';
@@ -128,6 +128,7 @@ export class DefaultCompletionProvider implements CompletionProvider {
128128
protected readonly nodeKindProvider: NodeKindProvider;
129129
protected readonly fuzzyMatcher: FuzzyMatcher;
130130
protected readonly grammarConfig: GrammarConfig;
131+
protected readonly astReflection: AstReflection;
131132

132133
constructor(services: LangiumServices) {
133134
this.scopeProvider = services.references.ScopeProvider;
@@ -138,6 +139,7 @@ export class DefaultCompletionProvider implements CompletionProvider {
138139
this.nodeKindProvider = services.shared.lsp.NodeKindProvider;
139140
this.fuzzyMatcher = services.shared.lsp.FuzzyMatcher;
140141
this.grammarConfig = services.parser.GrammarConfig;
142+
this.astReflection = services.shared.AstReflection;
141143
}
142144

143145
async getCompletion(document: LangiumDocument, params: CompletionParams): Promise<CompletionList | undefined> {
@@ -200,7 +202,6 @@ export class DefaultCompletionProvider implements CompletionProvider {
200202
const parserRule = getEntryRule(this.grammar)!;
201203
const firstFeatures = findFirstFeatures({
202204
feature: parserRule.definition,
203-
new: true,
204205
type: getExplicitRuleType(parserRule)
205206
});
206207
if (tokens.length > 0) {
@@ -373,37 +374,36 @@ export class DefaultCompletionProvider implements CompletionProvider {
373374
};
374375
}
375376

376-
protected async completionForRule(context: CompletionContext, rule: ast.AbstractRule, acceptor: CompletionAcceptor): Promise<void> {
377-
if (ast.isParserRule(rule)) {
378-
const firstFeatures = findFirstFeatures(rule.definition);
379-
await Promise.all(firstFeatures.map(next => this.completionFor(context, next, acceptor)));
380-
}
381-
}
382-
383377
protected completionFor(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): MaybePromise<void> {
384378
if (ast.isKeyword(next.feature)) {
385379
return this.completionForKeyword(context, next.feature, acceptor);
386380
} else if (ast.isCrossReference(next.feature) && context.node) {
387381
return this.completionForCrossReference(context, next as NextFeature<ast.CrossReference>, acceptor);
388382
}
383+
// Don't offer any completion for other elements (i.e. terminals, datatype rules)
384+
// We - from a framework level - cannot reasonably assume their contents.
385+
// Adopters can just override `completionFor` if they want to do that anyway.
389386
}
390387

391-
protected completionForCrossReference(context: CompletionContext, crossRef: NextFeature<ast.CrossReference>, acceptor: CompletionAcceptor): MaybePromise<void> {
392-
const assignment = getContainerOfType(crossRef.feature, ast.isAssignment);
388+
protected completionForCrossReference(context: CompletionContext, next: NextFeature<ast.CrossReference>, acceptor: CompletionAcceptor): MaybePromise<void> {
389+
const assignment = getContainerOfType(next.feature, ast.isAssignment);
393390
let node = context.node;
394391
if (assignment && node) {
395-
if (crossRef.type && (crossRef.new || node.$type !== crossRef.type)) {
392+
if (next.type) {
393+
// When `type` is set, it indicates that we have just entered a new parser rule.
394+
// The cross reference that we're trying to complete is on a new element that doesn't exist yet.
395+
// So we create a new synthetic element with the correct type information.
396396
node = {
397-
$type: crossRef.type,
397+
$type: next.type,
398398
$container: node,
399-
$containerProperty: crossRef.property
399+
$containerProperty: next.property
400400
};
401-
}
402-
if (!context) {
403-
return;
401+
assignMandatoryAstProperties(this.astReflection, node);
404402
}
405403
const refInfo: ReferenceInfo = {
406-
reference: {} as Reference,
404+
reference: {
405+
$refText: ''
406+
},
407407
container: node,
408408
property: assignment.feature
409409
};
@@ -452,7 +452,7 @@ export class DefaultCompletionProvider implements CompletionProvider {
452452
});
453453
}
454454

455-
protected filterKeyword(_context: CompletionContext, keyword: ast.Keyword): boolean {
455+
protected filterKeyword(context: CompletionContext, keyword: ast.Keyword): boolean {
456456
// Filter out keywords that do not contain any word character
457457
return keyword.value.match(/[\w]/) !== null;
458458
}

packages/langium/src/lsp/completion/follow-element-computation.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ export interface NextFeature<T extends ast.AbstractElement = ast.AbstractElement
2525
* The container property for the new `type`
2626
*/
2727
property?: string
28-
/**
29-
* Determines whether this `feature` is directly preceded by a new object declaration (such as an action or a rule call)
30-
*/
31-
new?: boolean
3228
}
3329

3430
/**
@@ -77,8 +73,7 @@ function findNextFeaturesInternal(options: { next: NextFeature, cardinalities: M
7773
const repeatingFeatures = findFirstFeaturesInternal({
7874
next: {
7975
feature: item,
80-
type: next.type,
81-
new: false
76+
type: next.type
8277
},
8378
cardinalities,
8479
visited,
@@ -95,8 +90,7 @@ function findNextFeaturesInternal(options: { next: NextFeature, cardinalities: M
9590
if (ownIndex !== undefined && ownIndex < parent.elements.length - 1) {
9691
features.push(...findNextFeaturesInGroup({
9792
feature: parent,
98-
type: next.type,
99-
new: false
93+
type: next.type
10094
}, ownIndex + 1, cardinalities, visited, plus));
10195
}
10296
// Try to find the next elements of the parent
@@ -105,8 +99,7 @@ function findNextFeaturesInternal(options: { next: NextFeature, cardinalities: M
10599
features.push(...findNextFeaturesInternal({
106100
next: {
107101
feature: parent,
108-
type: next.type,
109-
new: false
102+
type: next.type
110103
},
111104
cardinalities,
112105
visited,
@@ -146,7 +139,11 @@ function findFirstFeaturesInternal(options: { next: NextFeature, cardinalities:
146139
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
147140
} else if (ast.isAlternatives(feature) || ast.isUnorderedGroup(feature)) {
148141
return feature.elements.flatMap(e => findFirstFeaturesInternal({
149-
next: { feature: e, new: false, type },
142+
next: {
143+
feature: e,
144+
type,
145+
property: next.property
146+
},
150147
cardinalities,
151148
visited,
152149
plus
@@ -155,7 +152,6 @@ function findFirstFeaturesInternal(options: { next: NextFeature, cardinalities:
155152
} else if (ast.isAssignment(feature)) {
156153
const assignmentNext = {
157154
feature: feature.terminal,
158-
new: false,
159155
type,
160156
property: next.property ?? feature.feature
161157
};
@@ -165,7 +161,6 @@ function findFirstFeaturesInternal(options: { next: NextFeature, cardinalities:
165161
return findNextFeaturesInternal({
166162
next: {
167163
feature,
168-
new: true,
169164
type: getTypeName(feature),
170165
property: next.property ?? feature.feature
171166
},
@@ -177,8 +172,7 @@ function findFirstFeaturesInternal(options: { next: NextFeature, cardinalities:
177172
const rule = feature.rule.ref;
178173
const ruleCallNext = {
179174
feature: rule.definition,
180-
new: true,
181-
type: rule.fragment ? undefined : getExplicitRuleType(rule) ?? rule.name,
175+
type: rule.fragment || rule.dataType ? undefined : (getExplicitRuleType(rule) ?? rule.name),
182176
property: next.property
183177
};
184178
return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
@@ -204,7 +198,11 @@ function findNextFeaturesInGroup(next: NextFeature<ast.Group>, index: number, ca
204198
const features: NextFeature[] = [];
205199
let firstFeature: NextFeature;
206200
while (index < next.feature.elements.length) {
207-
firstFeature = { feature: next.feature.elements[index++], new: false, type: next.type };
201+
const feature = next.feature.elements[index++];
202+
firstFeature = {
203+
feature,
204+
type: next.type
205+
};
208206
features.push(...findFirstFeaturesInternal({
209207
next: firstFeature,
210208
cardinalities,

packages/langium/src/parser/langium-parser.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { defaultParserErrorProvider, EmbeddedActionsParser, LLkLookaheadStrategy
1717
import { LLStarLookaheadStrategy } from 'chevrotain-allstar';
1818
import { isAssignment, isCrossReference, isKeyword } from '../grammar/generated/ast.js';
1919
import { getTypeName, isDataTypeRule } from '../grammar/internal-grammar-util.js';
20-
import { getContainerOfType, linkContentToContainer } from '../utils/ast-util.js';
20+
import { assignMandatoryAstProperties, getContainerOfType, linkContentToContainer } from '../utils/ast-util.js';
2121
import { CstNodeBuilder } from './cst-node-builder.js';
2222

2323
export type ParseResult<T = AstNode> = {
@@ -275,23 +275,11 @@ export class LangiumParser extends AbstractLangiumParser {
275275
if (isDataTypeNode(obj)) {
276276
return this.converter.convert(obj.value, obj.$cstNode);
277277
} else {
278-
this.assignMandatoryProperties(obj);
278+
assignMandatoryAstProperties(this.astReflection, obj);
279279
}
280280
return obj;
281281
}
282282

283-
private assignMandatoryProperties(obj: any): void {
284-
const typeMetaData = this.astReflection.getTypeMetaData(obj.$type);
285-
for (const mandatoryProperty of typeMetaData.mandatory) {
286-
const value = obj[mandatoryProperty.name];
287-
if (mandatoryProperty.type === 'array' && !Array.isArray(value)) {
288-
obj[mandatoryProperty.name] = [];
289-
} else if (mandatoryProperty.type === 'boolean' && value === undefined) {
290-
obj[mandatoryProperty.name] = false;
291-
}
292-
}
293-
}
294-
295283
private getAssignment(feature: AbstractElement): AssignmentElement {
296284
if (!this.assignmentMap.has(feature)) {
297285
const assignment = getContainerOfType(feature, isAssignment);

packages/langium/src/utils/ast-util.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
******************************************************************************/
66

77
import type { Range } from 'vscode-languageserver';
8-
import type { AstNode, CstNode, GenericAstNode, Reference, ReferenceInfo } from '../syntax-tree.js';
8+
import type { AstNode, AstReflection, CstNode, GenericAstNode, Reference, ReferenceInfo } from '../syntax-tree.js';
99
import type { Stream, TreeStream } from '../utils/stream.js';
1010
import type { LangiumDocument } from '../workspace/documents.js';
1111
import { isAstNode, isReference } from '../syntax-tree.js';
@@ -232,6 +232,25 @@ export function findLocalReferences(targetNode: AstNode, lookup = getDocument(ta
232232
return stream(refs);
233233
}
234234

235+
/**
236+
* Assigns all mandatory AST properties to the specified node.
237+
*
238+
* @param reflection Reflection object used to gather mandatory properties for the node.
239+
* @param node Specified node is modified in place and properties are directly assigned.
240+
*/
241+
export function assignMandatoryAstProperties(reflection: AstReflection, node: AstNode): void {
242+
const typeMetaData = reflection.getTypeMetaData(node.$type);
243+
const genericNode = node as GenericAstNode;
244+
for (const mandatoryProperty of typeMetaData.mandatory) {
245+
const value = genericNode[mandatoryProperty.name];
246+
if (mandatoryProperty.type === 'array' && !Array.isArray(value)) {
247+
genericNode[mandatoryProperty.name] = [];
248+
} else if (mandatoryProperty.type === 'boolean' && value === undefined) {
249+
genericNode[mandatoryProperty.name] = false;
250+
}
251+
}
252+
}
253+
235254
/**
236255
* Creates a deep copy of the specified AST node.
237256
* The resulting copy will only contain semantically relevant information, such as the `$type` property and AST properties.

0 commit comments

Comments
 (0)