langium
Version:
A language engineering tool for the Language Server Protocol
159 lines (141 loc) • 6.86 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 { URI } from '../utils/uri-utils.js';
import type { NameProvider } from '../references/name-provider.js';
import type { LangiumCoreServices } from '../services.js';
import type { AstNode, AstNodeDescription, ReferenceInfo } from '../syntax-tree.js';
import type { AstNodeLocator } from './ast-node-locator.js';
import type { DocumentSegment, LangiumDocument } from './documents.js';
import { CancellationToken } from '../utils/cancellation.js';
import { isMultiReference, isReference } from '../syntax-tree.js';
import { getDocument, streamAst, streamReferences } from '../utils/ast-utils.js';
import { toDocumentSegment } from '../utils/cst-utils.js';
import { interruptAndCheck } from '../utils/promise-utils.js';
import { UriUtils } from '../utils/uri-utils.js';
/**
* Language-specific service for creating descriptions of AST nodes to be used for cross-reference resolutions.
*/
export interface AstNodeDescriptionProvider {
/**
* Create a description for the given AST node. This service method is typically used while indexing
* the contents of a document and during scope computation.
*
* @param node An AST node.
* @param name The name to be used to refer to the AST node. By default, this is determined by the
* `NameProvider` service, but alternative names may be provided according to the semantics
* of your language.
* @param document The document containing the AST node. If omitted, it is taken from the root AST node.
*/
createDescription(node: AstNode, name: string | undefined, document?: LangiumDocument): AstNodeDescription;
}
export class DefaultAstNodeDescriptionProvider implements AstNodeDescriptionProvider {
protected readonly astNodeLocator: AstNodeLocator;
protected readonly nameProvider: NameProvider;
constructor(services: LangiumCoreServices) {
this.astNodeLocator = services.workspace.AstNodeLocator;
this.nameProvider = services.references.NameProvider;
}
createDescription(node: AstNode, name: string | undefined, document?: LangiumDocument): AstNodeDescription {
const doc = document ?? getDocument(node);
name ??= this.nameProvider.getName(node);
const path = this.astNodeLocator.getAstNodePath(node);
if (!name) {
throw new Error(`Node at path ${path} has no name.`);
}
let nameNodeSegment: DocumentSegment | undefined;
const nameSegmentGetter = () => nameNodeSegment ??= toDocumentSegment(this.nameProvider.getNameNode(node) ?? node.$cstNode);
return {
node,
name,
get nameSegment() {
return nameSegmentGetter();
},
selectionSegment: toDocumentSegment(node.$cstNode),
type: node.$type,
documentUri: doc.uri,
path
};
}
}
/**
* Describes a cross-reference within a document or between two documents.
*/
export interface ReferenceDescription {
/** URI of the document that holds a reference */
sourceUri: URI
/** Path to AstNode that holds a reference */
sourcePath: string
/** Target document uri */
targetUri: URI
/** Path to the target AstNode inside the document */
targetPath: string
/** Segment of the reference text. */
segment: DocumentSegment
/** Marks a local reference i.e. a cross reference inside a document. */
local?: boolean
}
/**
* Language-specific service to create descriptions of all cross-references in a document. These are used by the `IndexManager`
* to determine which documents are affected and should be rebuilt when a document is changed.
*/
export interface ReferenceDescriptionProvider {
/**
* Create descriptions of all cross-references found in the given document. These descriptions are
* gathered by the `IndexManager` and stored in the global index so they can be considered when
* a document change is reported by the client.
*
* @param document The document in which to gather cross-references.
* @param cancelToken Indicates when to cancel the current operation.
* @throws `OperationCanceled` if a user action occurs during execution
*/
createDescriptions(document: LangiumDocument, cancelToken?: CancellationToken): Promise<ReferenceDescription[]>;
}
export class DefaultReferenceDescriptionProvider implements ReferenceDescriptionProvider {
protected readonly nodeLocator: AstNodeLocator;
constructor(services: LangiumCoreServices) {
this.nodeLocator = services.workspace.AstNodeLocator;
}
async createDescriptions(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<ReferenceDescription[]> {
const descr: ReferenceDescription[] = [];
const rootNode = document.parseResult.value;
for (const astNode of streamAst(rootNode)) {
await interruptAndCheck(cancelToken);
streamReferences(astNode).forEach(refInfo => {
if (!refInfo.reference.error) {
descr.push(...this.createInfoDescriptions(refInfo));
}
});
}
return descr;
}
protected createInfoDescriptions(refInfo: ReferenceInfo): ReferenceDescription[] {
const reference = refInfo.reference;
if (reference.error || !reference.$refNode) {
return [];
}
let items: AstNodeDescription[] = [];
if (isReference(reference) && reference.$nodeDescription) {
items = [reference.$nodeDescription];
} else if (isMultiReference(reference)) {
items = reference.items.map(e => e.$nodeDescription).filter(e => e !== undefined);
}
const sourceUri = getDocument(refInfo.container).uri;
const sourcePath = this.nodeLocator.getAstNodePath(refInfo.container);
const descriptions: ReferenceDescription[] = [];
const segment = toDocumentSegment(reference.$refNode);
for (const item of items) {
descriptions.push({
sourceUri,
sourcePath,
targetUri: item.documentUri,
targetPath: item.path,
segment,
local: UriUtils.equals(item.documentUri, sourceUri)
});
}
return descriptions;
}
}