langium
Version:
A language engineering tool for the Language Server Protocol
313 lines (297 loc) • 12.9 kB
text/typescript
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import type { IToken } from 'chevrotain';
import * as ast from '../../languages/generated/ast.js';
import { isAstNode } from '../../syntax-tree.js';
import { getContainerOfType } from '../../utils/ast-utils.js';
import type { Cardinality } from '../../utils/grammar-utils.js';
import { getCrossReferenceTerminal, getExplicitRuleType, getTypeName, isArrayCardinality, isOptionalCardinality, terminalRegex } from '../../utils/grammar-utils.js';
export interface NextFeature<T extends ast.AbstractElement = ast.AbstractElement> {
/**
* A feature that could appear during completion.
*/
feature: T
/**
* The type that carries this `feature`. Only set if we encounter a new type.
*/
type?: string
/**
* The container property for the new `type`
*/
property?: string
}
/**
* Calculates any features that can follow the given feature stack.
* This also includes features following optional features and features from previously called rules that could follow the last feature.
* @param featureStack A stack of features starting at the entry rule and ending at the feature of the current cursor position.
* @param unparsedTokens All tokens which haven't been parsed successfully yet. This is the case when we call this function inside an alternative.
* @returns Any `AbstractElement` that could be following the given feature stack.
*/
export function findNextFeatures(featureStack: NextFeature[][], unparsedTokens: IToken[]): NextFeature[] {
const context: InterpretationContext = {
stacks: featureStack,
tokens: unparsedTokens
};
interpretTokens(context);
// Reset the container property
context.stacks.flat().forEach(feature => { feature.property = undefined; });
const nextStacks = findNextFeatureStacks(context.stacks);
// We only need the last element of each stack
return nextStacks.map(e => e[e.length - 1]);
}
function findNextFeaturesInternal(options: { next: NextFeature, cardinalities: Map<ast.AbstractElement, Cardinality>, visited: Set<ast.AbstractElement>, plus: Set<ast.AbstractElement> }): NextFeature[] {
const { next, cardinalities, visited, plus } = options;
const features: NextFeature[] = [];
const feature = next.feature;
if (visited.has(feature)) {
return [];
} else if (!ast.isGroup(feature)) {
// Do not add the feature to the list if it is a group
// `findFirstFeaturesInternal` will take care of this
visited.add(feature);
}
let parent: ast.Group | undefined;
let item = feature;
while (item.$container) {
if (ast.isGroup(item.$container)) {
parent = item.$container;
break;
} else if (ast.isAbstractElement(item.$container)) {
item = item.$container;
} else {
break;
}
}
// First try to iterate the same element again
if (isArrayCardinality(item.cardinality)) {
const repeatingFeatures = findFirstFeaturesInternal({
next: {
feature: item,
type: next.type
},
cardinalities,
visited,
plus
});
for (const repeatingFeature of repeatingFeatures) {
plus.add(repeatingFeature.feature);
}
features.push(...repeatingFeatures);
}
if (parent) {
const ownIndex = parent.elements.indexOf(item);
// Find next elements of the same group
if (ownIndex !== undefined && ownIndex < parent.elements.length - 1) {
features.push(...findNextFeaturesInGroup({
feature: parent,
type: next.type
}, ownIndex + 1, cardinalities, visited, plus));
}
// Try to find the next elements of the parent
// Only do this if every following element is either optional or has been parsed as +
if (features.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)) || plus.has(e.feature))) {
features.push(...findNextFeaturesInternal({
next: {
feature: parent,
type: next.type
},
cardinalities,
visited,
plus
}));
}
}
return features;
}
/**
* Calculates the first child feature of any `AbstractElement`.
* @param next The `AbstractElement` whose first child features should be calculated.
*/
export function findFirstFeatures(next: ast.AbstractElement | NextFeature): NextFeature[] {
if (isAstNode(next)) {
next = { feature: next };
}
return findFirstFeaturesInternal({ next, cardinalities: new Map(), visited: new Set(), plus: new Set() });
}
function findFirstFeaturesInternal(options: { next: NextFeature, cardinalities: Map<ast.AbstractElement, Cardinality>, visited: Set<ast.AbstractElement>, plus: Set<ast.AbstractElement> }): NextFeature[] {
const { next, cardinalities, visited, plus } = options;
if (next === undefined) {
return [];
}
const { feature, type } = next;
if (ast.isGroup(feature)) {
if (visited.has(feature)) {
return [];
} else {
visited.add(feature);
}
return findNextFeaturesInGroup(next as NextFeature<ast.Group>, 0, cardinalities, visited, plus)
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
} else if (ast.isAlternatives(feature) || ast.isUnorderedGroup(feature)) {
return feature.elements.flatMap(e => findFirstFeaturesInternal({
next: {
feature: e,
type,
property: next.property
},
cardinalities,
visited,
plus
}))
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
} else if (ast.isAssignment(feature)) {
const assignmentNext = {
feature: feature.terminal,
type,
property: next.property ?? feature.feature
};
return findFirstFeaturesInternal({ next: assignmentNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
} else if (ast.isAction(feature)) {
return findNextFeaturesInternal({
next: {
feature,
type: getTypeName(feature),
property: next.property ?? feature.feature
},
cardinalities,
visited,
plus
});
} else if (ast.isRuleCall(feature) && ast.isParserRule(feature.rule.ref)) {
const rule = feature.rule.ref;
const ruleCallNext = {
feature: rule.definition,
type: rule.fragment || rule.dataType ? undefined : (getExplicitRuleType(rule) ?? rule.name),
property: next.property
};
return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
} else if (ast.isRuleCall(feature) && ast.isInfixRule(feature.rule.ref)) {
const rule = feature.rule.ref;
const call = rule.call.rule.ref;
if (!ast.isParserRule(call)) {
console.error('Failed to resolve reference to ' + rule.call.rule.$refText);
return [];
}
const ruleCallNext = {
feature: call.definition,
type: getExplicitRuleType(call) ?? call.name,
property: 'parts'
};
return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
} else {
return [next];
}
}
/**
* Modifying the cardinality is necessary to identify which features are coming from an optional feature.
* Those features should be optional as well.
* @param next The next feature that could be made optionally.
* @param cardinality The cardinality of the calling (parent) object.
* @returns A new feature that could be now optional (`?` or `*`).
*/
function modifyCardinality(next: NextFeature, cardinality: Cardinality, cardinalities: Map<ast.AbstractElement, Cardinality>): NextFeature {
cardinalities.set(next.feature, cardinality);
return next;
}
function findNextFeaturesInGroup(next: NextFeature<ast.Group>, index: number, cardinalities: Map<ast.AbstractElement, Cardinality>, visited: Set<ast.AbstractElement>, plus: Set<ast.AbstractElement>): NextFeature[] {
const features: NextFeature[] = [];
let firstFeature: NextFeature;
while (index < next.feature.elements.length) {
const feature = next.feature.elements[index++];
firstFeature = {
feature,
type: next.type
};
features.push(...findFirstFeaturesInternal({
next: firstFeature,
cardinalities,
visited,
plus
}));
if (!isOptionalCardinality(firstFeature.feature.cardinality ?? cardinalities.get(firstFeature.feature), firstFeature.feature)) {
break;
}
}
return features;
}
interface InterpretationContext {
tokens: IToken[]
stacks: NextFeature[][]
}
function interpretTokens(context: InterpretationContext): void {
for (const token of context.tokens) {
const nextFeatureStacks = findNextFeatureStacks(context.stacks, token);
context.stacks = nextFeatureStacks;
}
}
function findNextFeatureStacks(stacks: NextFeature[][], token?: IToken): NextFeature[][] {
const newStacks: NextFeature[][] = [];
for (const stack of stacks) {
newStacks.push(...interpretStackToken(stack, token));
}
return newStacks;
}
function interpretStackToken(stack: NextFeature[], token?: IToken): NextFeature[][] {
const cardinalities = new Map<ast.AbstractElement, Cardinality>();
const plus = new Set<ast.AbstractElement>(stack.map(e => e.feature).filter(isPlusFeature));
const newStacks: NextFeature[][] = [];
while (stack.length > 0) {
const top = stack.pop()!;
const allNextFeatures = findNextFeaturesInternal({
next: top,
cardinalities,
plus,
visited: new Set()
}).filter(next => token ? featureMatches(next.feature, token) : true);
for (const nextFeature of allNextFeatures) {
newStacks.push([...stack, nextFeature]);
}
if (!allNextFeatures.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)))) {
break;
}
}
return newStacks;
}
function isPlusFeature(feature: ast.AbstractElement): boolean {
if (feature.cardinality === '+') {
return true;
}
const assignment = getContainerOfType(feature, ast.isAssignment);
if (assignment && assignment.cardinality === '+') {
return true;
}
return false;
}
function featureMatches(feature: ast.AbstractElement, token: IToken): boolean {
if (ast.isKeyword(feature)) {
const content = feature.value;
return content === token.image;
} else if (ast.isRuleCall(feature)) {
return ruleMatches(feature.rule.ref, token);
} else if (ast.isCrossReference(feature)) {
const crossRefTerminal = getCrossReferenceTerminal(feature);
if (crossRefTerminal) {
return featureMatches(crossRefTerminal, token);
}
}
return false;
}
function ruleMatches(rule: ast.AbstractRule | undefined, token: IToken): boolean {
if (ast.isParserRule(rule)) {
const ruleFeatures = findFirstFeatures(rule.definition);
return ruleFeatures.some(e => featureMatches(e.feature, token));
} else if (ast.isTerminalRule(rule)) {
// We have to take keywords into account
// e.g. most keywords are valid IDs as well
// Only return 'true' if this terminal does not match a keyword. TODO
return terminalRegex(rule).test(token.image);
} else {
return false;
}
}