langium
Version:
A language engineering tool for the Language Server Protocol
221 lines (195 loc) • 8.18 kB
text/typescript
/******************************************************************************
* Copyright 2023 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 { AstNodeDescription } from '../syntax-tree.js';
import { MultiMap } from '../utils/collections.js';
import type { Stream } from '../utils/stream.js';
import { EMPTY_STREAM, stream } from '../utils/stream.js';
/**
* A scope describes what target elements are visible from a specific cross-reference context.
*/
export interface Scope {
/**
* Find a target element matching the given name. If no element is found, `undefined` is returned.
* If multiple matching elements are present, the selection of the returned element should be done
* according to the semantics of your language. Usually it is the element that is most closely defined.
*
* @param name Name of the cross-reference target as it appears in the source text.
*/
getElement(name: string): AstNodeDescription | undefined;
/**
* Finds all target elements matching the given name. If no element is found, an empty stream is returned.
*
* @param name Name of the cross-reference target as it appears in the source text.
*/
getElements(name: string): Stream<AstNodeDescription>;
/**
* Create a stream of all elements in the scope. This is used to compute completion proposals to be
* shown in the editor.
*/
getAllElements(): Stream<AstNodeDescription>;
}
export interface ScopeOptions {
/**
* Whether the scope should be case insensitive.
* Defaults to `false`.
*/
caseInsensitive?: boolean;
/**
* Whether the outer scope should be concatenated with the local scope when calling `getElements`.
* Defaults to `true`.
*/
concatOuterScope?: boolean;
}
/**
* The default scope implementation is based on a `Stream`. It has an optional _outer scope_ describing
* the next level of elements, which are queried when a target element is not found in the stream provided
* to this scope.
*/
export class StreamScope implements Scope {
readonly elements: Stream<AstNodeDescription>;
readonly outerScope?: Scope;
readonly caseInsensitive: boolean;
readonly concatOuterScope: boolean;
constructor(elements: Stream<AstNodeDescription>, outerScope?: Scope, options?: ScopeOptions) {
this.elements = elements;
this.outerScope = outerScope;
this.caseInsensitive = options?.caseInsensitive ?? false;
this.concatOuterScope = options?.concatOuterScope ?? true;
}
getAllElements(): Stream<AstNodeDescription> {
if (this.outerScope) {
return this.elements.concat(this.outerScope.getAllElements());
} else {
return this.elements;
}
}
getElement(name: string): AstNodeDescription | undefined {
const lowerCaseName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.caseInsensitive
? this.elements.find(e => e.name.toLowerCase() === lowerCaseName)
: this.elements.find(e => e.name === name);
if (local) {
return local;
}
if (this.outerScope) {
return this.outerScope.getElement(name);
}
return undefined;
}
getElements(name: string): Stream<AstNodeDescription> {
const lowerCaseName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.caseInsensitive
? this.elements.filter(e => e.name.toLowerCase() === lowerCaseName)
: this.elements.filter(e => e.name === name);
if ((this.concatOuterScope || local.isEmpty()) && this.outerScope) {
return local.concat(this.outerScope.getElements(name));
} else {
return local;
}
}
}
export class MapScope implements Scope {
readonly elements: Map<string, AstNodeDescription>;
readonly outerScope?: Scope;
readonly caseInsensitive: boolean;
readonly concatOuterScope: boolean;
constructor(elements: Iterable<AstNodeDescription>, outerScope?: Scope, options?: ScopeOptions) {
this.elements = new Map();
this.caseInsensitive = options?.caseInsensitive ?? false;
this.concatOuterScope = options?.concatOuterScope ?? true;
for (const element of elements) {
const name = this.caseInsensitive
? element.name.toLowerCase()
: element.name;
this.elements.set(name, element);
}
this.outerScope = outerScope;
}
getElement(name: string): AstNodeDescription | undefined {
const localName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.elements.get(localName);
if (local) {
return local;
}
if (this.outerScope) {
return this.outerScope.getElement(name);
}
return undefined;
}
getElements(name: string): Stream<AstNodeDescription> {
const localName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.elements.get(localName);
const arr = local ? [local] : [];
if ((this.concatOuterScope || arr.length > 0) && this.outerScope) {
return stream(arr).concat(this.outerScope.getElements(name));
} else {
return stream(arr);
}
}
getAllElements(): Stream<AstNodeDescription> {
let elementStream = stream(this.elements.values());
if (this.outerScope) {
elementStream = elementStream.concat(this.outerScope.getAllElements());
}
return elementStream;
}
}
export class MultiMapScope implements Scope {
readonly elements: MultiMap<string, AstNodeDescription>;
readonly outerScope?: Scope;
readonly caseInsensitive: boolean;
readonly concatOuterScope: boolean;
constructor(elements: Iterable<AstNodeDescription>, outerScope?: Scope, options?: ScopeOptions) {
this.elements = new MultiMap();
this.caseInsensitive = options?.caseInsensitive ?? false;
this.concatOuterScope = options?.concatOuterScope ?? true;
for (const element of elements) {
const name = this.caseInsensitive
? element.name.toLowerCase()
: element.name;
this.elements.add(name, element);
}
this.outerScope = outerScope;
}
getElement(name: string): AstNodeDescription | undefined {
const localName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.elements.get(localName)[0];
if (local) {
return local;
}
if (this.outerScope) {
return this.outerScope.getElement(name);
}
return undefined;
}
getElements(name: string): Stream<AstNodeDescription> {
const localName = this.caseInsensitive ? name.toLowerCase() : name;
const local = this.elements.get(localName);
if ((this.concatOuterScope || local.length === 0) && this.outerScope) {
return stream(local).concat(this.outerScope.getElements(name));
} else {
return stream(local);
}
}
getAllElements(): Stream<AstNodeDescription> {
let elementStream = stream(this.elements.values());
if (this.outerScope) {
elementStream = elementStream.concat(this.outerScope.getAllElements());
}
return elementStream;
}
}
export const EMPTY_SCOPE: Scope = {
getElement(): undefined {
return undefined;
},
getElements(): Stream<AstNodeDescription> {
return EMPTY_STREAM;
},
getAllElements(): Stream<AstNodeDescription> {
return EMPTY_STREAM;
}
};