完成世界书、骰子、apiconfig页面处理
This commit is contained in:
179
frontend/node_modules/langium/src/workspace/configuration.ts
generated
vendored
Normal file
179
frontend/node_modules/langium/src/workspace/configuration.ts
generated
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
/******************************************************************************
|
||||
* Copyright 2022 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 { Emitter } from '../utils/event.js';
|
||||
import type {
|
||||
ConfigurationItem,
|
||||
DidChangeConfigurationParams,
|
||||
DidChangeConfigurationRegistrationOptions,
|
||||
Disposable,
|
||||
Event,
|
||||
InitializeParams,
|
||||
InitializedParams
|
||||
} from 'vscode-languageserver-protocol';
|
||||
import type { ServiceRegistry } from '../service-registry.js';
|
||||
import type { LangiumSharedCoreServices } from '../services.js';
|
||||
import { Deferred } from '../utils/promise-utils.js';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface ConfigurationProvider {
|
||||
|
||||
/**
|
||||
* A promise that resolves when the configuration provider is ready to be used.
|
||||
*/
|
||||
readonly ready: Promise<void>;
|
||||
|
||||
/**
|
||||
* When used in a language server context, this method is called when the server receives
|
||||
* the `initialize` request.
|
||||
*/
|
||||
initialize(params: InitializeParams): void;
|
||||
|
||||
/**
|
||||
* When used in a language server context, this method is called when the server receives
|
||||
* the `initialized` notification.
|
||||
*/
|
||||
initialized(params: ConfigurationInitializedParams): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns a configuration value stored for the given language.
|
||||
*
|
||||
* @param language The language id
|
||||
* @param configuration Configuration name
|
||||
*/
|
||||
getConfiguration(language: string, configuration: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Updates the cached configurations using the `change` notification parameters.
|
||||
*
|
||||
* @param change The parameters of a change configuration notification.
|
||||
* `settings` property of the change object could be expressed as `Record<string, Record<string, any>>`
|
||||
*/
|
||||
updateConfiguration(change: DidChangeConfigurationParams): void;
|
||||
|
||||
/**
|
||||
* Get notified after a configuration section has been updated.
|
||||
*/
|
||||
onConfigurationSectionUpdate(callback: ConfigurationSectionUpdateListener): Disposable
|
||||
}
|
||||
|
||||
export interface ConfigurationInitializedParams extends InitializedParams {
|
||||
register?: (params: DidChangeConfigurationRegistrationOptions) => void,
|
||||
fetchConfiguration?: (configuration: ConfigurationItem[]) => Promise<any>
|
||||
}
|
||||
|
||||
export interface ConfigurationSectionUpdate {
|
||||
/**
|
||||
* The name of the configuration section that has been updated.
|
||||
*/
|
||||
section: string;
|
||||
|
||||
/**
|
||||
* The updated configuration section.
|
||||
*/
|
||||
configuration: any;
|
||||
}
|
||||
|
||||
export type ConfigurationSectionUpdateListener = (update: ConfigurationSectionUpdate) => void;
|
||||
|
||||
/**
|
||||
* Base configuration provider for building up other configuration providers
|
||||
*/
|
||||
export class DefaultConfigurationProvider implements ConfigurationProvider {
|
||||
|
||||
protected readonly serviceRegistry: ServiceRegistry;
|
||||
protected readonly _ready = new Deferred<void>();
|
||||
protected readonly onConfigurationSectionUpdateEmitter = new Emitter<ConfigurationSectionUpdate>();
|
||||
protected settings: Record<string, Record<string, any>> = {};
|
||||
protected workspaceConfig = false;
|
||||
|
||||
constructor(services: LangiumSharedCoreServices) {
|
||||
this.serviceRegistry = services.ServiceRegistry;
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
return this._ready.promise;
|
||||
}
|
||||
|
||||
initialize(params: InitializeParams): void {
|
||||
this.workspaceConfig = params.capabilities.workspace?.configuration ?? false;
|
||||
}
|
||||
|
||||
async initialized(params: ConfigurationInitializedParams): Promise<void> {
|
||||
if (this.workspaceConfig) {
|
||||
if (params.register) {
|
||||
// params.register(...) is a function to be provided by the calling language server for the sake of
|
||||
// decoupling this implementation from the concrete LSP implementations, specifically the LSP Connection
|
||||
|
||||
const languages = this.serviceRegistry.all;
|
||||
params.register({
|
||||
// Listen to configuration changes for all languages
|
||||
section: languages.map(lang => this.toSectionName(lang.LanguageMetaData.languageId))
|
||||
});
|
||||
}
|
||||
|
||||
if (params.fetchConfiguration) {
|
||||
// params.fetchConfiguration(...) is a function to be provided by the calling language server for the sake of
|
||||
// decoupling this implementation from the concrete LSP implementations, specifically the LSP Connection
|
||||
const configToUpdate = this.serviceRegistry.all.map(lang => <ConfigurationItem>{
|
||||
// Fetch the configuration changes for all languages
|
||||
section: this.toSectionName(lang.LanguageMetaData.languageId)
|
||||
});
|
||||
|
||||
// get workspace configurations (default scope URI)
|
||||
const configs = await params.fetchConfiguration(configToUpdate);
|
||||
configToUpdate.forEach((conf, idx) => {
|
||||
this.updateSectionConfiguration(conf.section!, configs[idx]);
|
||||
});
|
||||
}
|
||||
}
|
||||
this._ready.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cached configurations using the `change` notification parameters.
|
||||
*
|
||||
* @param change The parameters of a change configuration notification.
|
||||
* `settings` property of the change object could be expressed as `Record<string, Record<string, any>>`
|
||||
*/
|
||||
updateConfiguration(change: DidChangeConfigurationParams): void {
|
||||
if (typeof change.settings !== 'object' || change.settings === null) {
|
||||
return;
|
||||
}
|
||||
Object.entries(change.settings).forEach(([section, configuration]) => {
|
||||
this.updateSectionConfiguration(section, configuration);
|
||||
this.onConfigurationSectionUpdateEmitter.fire({ section, configuration });
|
||||
});
|
||||
}
|
||||
|
||||
protected updateSectionConfiguration(section: string, configuration: any): void {
|
||||
this.settings[section] = configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a configuration value stored for the given language.
|
||||
*
|
||||
* @param language The language id
|
||||
* @param configuration Configuration name
|
||||
*/
|
||||
async getConfiguration(language: string, configuration: string): Promise<any> {
|
||||
await this.ready;
|
||||
|
||||
const sectionName = this.toSectionName(language);
|
||||
if (this.settings[sectionName]) {
|
||||
return this.settings[sectionName][configuration];
|
||||
}
|
||||
}
|
||||
|
||||
protected toSectionName(languageId: string): string {
|
||||
return `${languageId}`;
|
||||
}
|
||||
|
||||
get onConfigurationSectionUpdate(): Event<ConfigurationSectionUpdate> {
|
||||
return this.onConfigurationSectionUpdateEmitter.event;
|
||||
}
|
||||
}
|
||||
687
frontend/node_modules/langium/src/workspace/document-builder.ts
generated
vendored
Normal file
687
frontend/node_modules/langium/src/workspace/document-builder.ts
generated
vendored
Normal file
@@ -0,0 +1,687 @@
|
||||
/******************************************************************************
|
||||
* 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 { LSPErrorCodes, ResponseError } from 'vscode-languageserver-protocol';
|
||||
import { CancellationToken } from '../utils/cancellation.js';
|
||||
import { Disposable } from '../utils/disposable.js';
|
||||
import type { ServiceRegistry } from '../service-registry.js';
|
||||
import type { LangiumSharedCoreServices } from '../services.js';
|
||||
import type { AstNode } from '../syntax-tree.js';
|
||||
import type { MaybePromise } from '../utils/promise-utils.js';
|
||||
import type { Deferred } from '../utils/promise-utils.js';
|
||||
import type { ValidationOptions } from '../validation/document-validator.js';
|
||||
import type { IndexManager } from '../workspace/index-manager.js';
|
||||
import type { LangiumDocument, LangiumDocuments, LangiumDocumentFactory, TextDocumentProvider } from './documents.js';
|
||||
import { MultiMap } from '../utils/collections.js';
|
||||
import { OperationCancelled, interruptAndCheck, isOperationCancelled } from '../utils/promise-utils.js';
|
||||
import { stream } from '../utils/stream.js';
|
||||
import { UriUtils, type URI } from '../utils/uri-utils.js';
|
||||
import type { ValidationCategory } from '../validation/validation-registry.js';
|
||||
import { DocumentState } from './documents.js';
|
||||
import type { FileSystemProvider } from './file-system-provider.js';
|
||||
import type { WorkspaceManager } from './workspace-manager.js';
|
||||
|
||||
export interface BuildOptions {
|
||||
/**
|
||||
* Control the linking and references indexing phase with this option. The default if not specified is `true`.
|
||||
* If set to `false`, references can still be resolved - that's done lazily when you access the `ref` property of
|
||||
* a reference. But you won't get any diagnostics for linking errors and the references won't be considered
|
||||
* when updating other documents.
|
||||
*/
|
||||
eagerLinking?: boolean
|
||||
|
||||
/**
|
||||
* Control the validation phase with this option:
|
||||
* - `true` enables all validation checks and forces revalidating the documents
|
||||
* In order to include additional, custom validation categories, override `DefaultDocumentBuilder.getAllValidationCategories(...)`.
|
||||
* - `false` or `undefined` disables all validation checks
|
||||
* - An object runs only the necessary validation checks; the `categories` property restricts this to a specific subset (which might include custom categories as well).
|
||||
*/
|
||||
validation?: boolean | ValidationOptions
|
||||
}
|
||||
|
||||
export interface DocumentBuildState {
|
||||
/** Whether a document has completed its last build process. */
|
||||
completed: boolean
|
||||
/** The options used for the last build process. */
|
||||
options: BuildOptions
|
||||
/** Additional information about the last build result. */
|
||||
result?: {
|
||||
validationChecks?: ValidationCategory[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared-service for building and updating `LangiumDocument`s.
|
||||
*/
|
||||
export interface DocumentBuilder {
|
||||
|
||||
/** The options used for rebuilding documents after an update. */
|
||||
updateBuildOptions: BuildOptions;
|
||||
|
||||
/**
|
||||
* Execute all necessary build steps for the given documents.
|
||||
*
|
||||
* @param documents Set of documents to be built.
|
||||
* @param options Options for the document builder.
|
||||
* @param cancelToken Indicates when to cancel the current operation.
|
||||
* @throws `OperationCanceled` if a user action occurs during execution
|
||||
*/
|
||||
build<T extends AstNode>(documents: Array<LangiumDocument<T>>, options?: BuildOptions, cancelToken?: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* This method is called when a document change is detected. It updates the state of all
|
||||
* affected documents, including those with references to the changed ones, so they are rebuilt.
|
||||
*
|
||||
* @param changed URIs of changed or created documents
|
||||
* @param deleted URIs of deleted documents
|
||||
* @param cancelToken allows to cancel the current operation
|
||||
* @throws `OperationCancelled` if cancellation is detected during execution
|
||||
*/
|
||||
update(changed: URI[], deleted: URI[], cancelToken?: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Notify the given callback when a document update was triggered, but before any document
|
||||
* is rebuilt. Listeners to this event should not perform any long-running task.
|
||||
*/
|
||||
onUpdate(callback: DocumentUpdateListener): Disposable;
|
||||
|
||||
/**
|
||||
* Reset the state of a document to the specified state, removing any derived data as needed.
|
||||
*
|
||||
* @param document The document to reset.
|
||||
* @param state The state to reset the document to.
|
||||
*/
|
||||
resetToState<T extends AstNode>(document: LangiumDocument<T>, state: DocumentState): void;
|
||||
|
||||
/**
|
||||
* Notify the given callback when a set of documents has been built reaching the specified target state.
|
||||
*/
|
||||
onBuildPhase(targetState: DocumentState, callback: DocumentBuildListener): Disposable;
|
||||
|
||||
/**
|
||||
* Notify the specified callback when a document has been built reaching the specified target state.
|
||||
* Unlike {@link onBuildPhase} the listener is called for every single document.
|
||||
*
|
||||
* There are two main advantages compared to {@link onBuildPhase}:
|
||||
* 1. If the build is cancelled, {@link onDocumentPhase} will still fire for documents that have reached a specific state.
|
||||
* Meanwhile, {@link onBuildPhase} won't fire for that state.
|
||||
* 2. The {@link DocumentBuilder} ensures that all {@link DocumentPhaseListener} instances are called for a built document.
|
||||
* Even if the build is cancelled before those listeners were called.
|
||||
*/
|
||||
onDocumentPhase(targetState: DocumentState, callback: DocumentPhaseListener): Disposable;
|
||||
|
||||
/**
|
||||
* Wait until the workspace has reached the specified state for all documents.
|
||||
*
|
||||
* @param state The desired state. The promise won't resolve until all documents have reached this state
|
||||
* @param cancelToken Optionally allows to cancel the wait operation, disposing any listeners in the process
|
||||
* @throws `OperationCancelled` if cancellation has been requested before the state has been reached
|
||||
*/
|
||||
waitUntil(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Wait until the document specified by the {@link uri} has reached the specified state.
|
||||
*
|
||||
* @param state The desired state. The promise won't resolve until the document has reached this state.
|
||||
* @param uri The specified URI that points to the document. If the URI does not exist, the promise will resolve once the workspace has reached the specified state.
|
||||
* @param cancelToken Optionally allows to cancel the wait operation, disposing any listeners in the process.
|
||||
* @return The URI of the document that has reached the desired state, or `undefined` if the document does not exist.
|
||||
* @throws `OperationCancelled` if cancellation has been requested before the state has been reached
|
||||
*/
|
||||
waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken): Promise<URI | undefined>;
|
||||
}
|
||||
|
||||
export type DocumentUpdateListener = (changed: URI[], deleted: URI[]) => void | Promise<void>
|
||||
export type DocumentBuildListener = (built: LangiumDocument[], cancelToken: CancellationToken) => void | Promise<void>
|
||||
export type DocumentPhaseListener = (built: LangiumDocument, cancelToken: CancellationToken) => void | Promise<void>
|
||||
export class DefaultDocumentBuilder implements DocumentBuilder {
|
||||
|
||||
updateBuildOptions: BuildOptions = {
|
||||
// Default: run only the built-in validation checks and those in the _fast_ category (includes those without category)
|
||||
validation: {
|
||||
categories: ['built-in', 'fast']
|
||||
}
|
||||
};
|
||||
|
||||
protected readonly langiumDocuments: LangiumDocuments;
|
||||
protected readonly langiumDocumentFactory: LangiumDocumentFactory;
|
||||
protected readonly textDocuments: TextDocumentProvider | undefined;
|
||||
protected readonly indexManager: IndexManager;
|
||||
protected readonly fileSystemProvider: FileSystemProvider;
|
||||
protected readonly workspaceManager: () => WorkspaceManager;
|
||||
protected readonly serviceRegistry: ServiceRegistry;
|
||||
|
||||
protected readonly updateListeners: DocumentUpdateListener[] = [];
|
||||
protected readonly buildPhaseListeners = new MultiMap<DocumentState, DocumentBuildListener>();
|
||||
protected readonly documentPhaseListeners = new MultiMap<DocumentState, DocumentPhaseListener>();
|
||||
protected readonly buildState = new Map<string, DocumentBuildState>();
|
||||
protected readonly documentBuildWaiters = new Map<string, Deferred<void>>();
|
||||
protected currentState = DocumentState.Changed;
|
||||
|
||||
constructor(services: LangiumSharedCoreServices) {
|
||||
this.langiumDocuments = services.workspace.LangiumDocuments;
|
||||
this.langiumDocumentFactory = services.workspace.LangiumDocumentFactory;
|
||||
this.textDocuments = services.workspace.TextDocuments;
|
||||
this.indexManager = services.workspace.IndexManager;
|
||||
this.fileSystemProvider = services.workspace.FileSystemProvider;
|
||||
this.workspaceManager = () => services.workspace.WorkspaceManager;
|
||||
this.serviceRegistry = services.ServiceRegistry;
|
||||
}
|
||||
|
||||
async build<T extends AstNode>(documents: Array<LangiumDocument<T>>, options: BuildOptions = {}, cancelToken = CancellationToken.None): Promise<void> {
|
||||
for (const document of documents) {
|
||||
const key = document.uri.toString();
|
||||
if (document.state === DocumentState.Validated) {
|
||||
if (typeof options.validation === 'boolean' && options.validation) {
|
||||
// Force re-running all validation checks
|
||||
this.resetToState(document, DocumentState.IndexedReferences);
|
||||
} else if (typeof options.validation === 'object') {
|
||||
// Validation with explicit options was requested for a document that has already been partly validated.
|
||||
// In this case, we need to execute only the missing validation categories.
|
||||
const categories = this.findMissingValidationCategories(document, options);
|
||||
if (categories.length > 0) {
|
||||
// Validate this document, since some of the requested validation categories are not executed yet.
|
||||
// In all other cases/else-branches, the document is not build at all.
|
||||
this.buildState.set(key, {
|
||||
completed: false,
|
||||
options: {
|
||||
validation: {
|
||||
categories
|
||||
}
|
||||
},
|
||||
result: this.buildState.get(key)?.result,
|
||||
});
|
||||
// Reset the state, but keep the existing validation markers of the already completed validation categories.
|
||||
document.state = DocumentState.IndexedReferences;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default: forget any previous build options
|
||||
this.buildState.delete(key);
|
||||
}
|
||||
}
|
||||
this.currentState = DocumentState.Changed;
|
||||
await this.emitUpdate(documents.map(e => e.uri), []);
|
||||
await this.buildDocuments(documents, options, cancelToken);
|
||||
}
|
||||
|
||||
async update(changed: URI[], deleted: URI[], cancelToken = CancellationToken.None): Promise<void> {
|
||||
this.currentState = DocumentState.Changed;
|
||||
// Remove all metadata of documents that are reported as deleted
|
||||
const deletedUris: URI[] = [];
|
||||
for (const deletedUri of deleted) {
|
||||
// Since the deleted URI might point to a directory, we delete all documents within
|
||||
const deletedDocs = this.langiumDocuments.deleteDocuments(deletedUri);
|
||||
for (const doc of deletedDocs) {
|
||||
deletedUris.push(doc.uri);
|
||||
this.cleanUpDeleted(doc);
|
||||
}
|
||||
}
|
||||
// Since the changed URI might point to a directory, we need to check all (nested) documents in that directory
|
||||
const changedUris = (await Promise.all(changed.map(uri => this.findChangedUris(uri)))).flat();
|
||||
// Set the state of all changed documents to `Changed` so they are completely rebuilt
|
||||
for (const changedUri of changedUris) {
|
||||
let changedDocument = this.langiumDocuments.getDocument(changedUri);
|
||||
if (changedDocument === undefined) {
|
||||
// We create an unparsed, invalid document.
|
||||
// This will be parsed as soon as we reach the first document builder phase.
|
||||
// This allows to cancel the parsing process later in case we need it.
|
||||
changedDocument = this.langiumDocumentFactory.fromModel({ $type: 'INVALID' }, changedUri);
|
||||
changedDocument.state = DocumentState.Changed; // required, since `langiumDocumentFactory.fromModel` marks the new document as `DocumentState.Parsed`
|
||||
this.langiumDocuments.addDocument(changedDocument);
|
||||
}
|
||||
this.resetToState(changedDocument, DocumentState.Changed);
|
||||
}
|
||||
// Set the state of all documents that should be relinked to `ComputedScopes` (if not already lower)
|
||||
const allChangedUris = stream(changedUris).concat(deletedUris).map(uri => uri.toString()).toSet();
|
||||
this.langiumDocuments.all
|
||||
.filter(doc => !allChangedUris.has(doc.uri.toString()) && this.shouldRelink(doc, allChangedUris))
|
||||
.forEach(doc => this.resetToState(doc, DocumentState.ComputedScopes));
|
||||
// Notify listeners of the update
|
||||
await this.emitUpdate(changedUris, deletedUris);
|
||||
// Only allow interrupting the execution after all state changes are done
|
||||
await interruptAndCheck(cancelToken);
|
||||
|
||||
// Collect and sort all documents that we should rebuild
|
||||
const rebuildDocuments = this.sortDocuments(
|
||||
this.langiumDocuments.all
|
||||
.filter(doc =>
|
||||
// This includes those that were reported as changed and those that we selected for relinking
|
||||
doc.state < DocumentState.Validated
|
||||
// This includes those for which a previous build has been cancelled
|
||||
|| !this.buildState.get(doc.uri.toString())?.completed
|
||||
// `updateBuildOptions` changed between the last build (which is completed) and the current build,
|
||||
// leading to incomplete results, e.g. some validation categories are requested, which are not executed during the last build
|
||||
|| this.resultsAreIncomplete(doc, this.updateBuildOptions)
|
||||
)
|
||||
.toArray()
|
||||
);
|
||||
await this.buildDocuments(rebuildDocuments, this.updateBuildOptions, cancelToken);
|
||||
}
|
||||
|
||||
protected resultsAreIncomplete(document: LangiumDocument, options: BuildOptions | undefined): boolean {
|
||||
return this.findMissingValidationCategories(document, options).length >= 1;
|
||||
}
|
||||
|
||||
protected findMissingValidationCategories(document: LangiumDocument, options: BuildOptions | undefined): ValidationCategory[] {
|
||||
const state = this.buildState.get(document.uri.toString());
|
||||
const allCategories = this.serviceRegistry.getServices(document.uri).validation.ValidationRegistry.getAllValidationCategories(document);
|
||||
const executedCategories = state?.result?.validationChecks ? new Set(state?.result?.validationChecks) : state?.completed ? allCategories : new Set();
|
||||
const requestedCategories = (options === undefined || options.validation === true) ? allCategories
|
||||
: typeof options.validation === 'object' ? (options.validation.categories ?? allCategories) : [];
|
||||
return stream(requestedCategories).filter(requested => !executedCategories.has(requested)).toArray();
|
||||
}
|
||||
|
||||
protected async findChangedUris(changed: URI): Promise<URI[]> {
|
||||
// Most common case is that the document/textDocument at the specified URI has changed
|
||||
const document = this.langiumDocuments.getDocument(changed) ?? this.textDocuments?.get(changed);
|
||||
if (document) {
|
||||
return [changed];
|
||||
}
|
||||
// If the document doesn't exist yet, we need to check what kind of file has changed
|
||||
try {
|
||||
const stat = await this.fileSystemProvider.stat(changed);
|
||||
if (stat.isDirectory) {
|
||||
// If a directory has changed, we need to check all documents in that directory
|
||||
const uris = await this.workspaceManager().searchFolder(changed);
|
||||
return uris;
|
||||
} else if (this.workspaceManager().shouldIncludeEntry(stat)) {
|
||||
// Return the changed URI if it's a file that we can handle
|
||||
return [changed];
|
||||
}
|
||||
} catch {
|
||||
// If we can't determine the file type, we discard the change
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected async emitUpdate(changed: URI[], deleted: URI[]): Promise<void> {
|
||||
await Promise.all(this.updateListeners.map(listener => listener(changed, deleted)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given documents by priority. By default, documents with an open text document are prioritized.
|
||||
* This is useful to ensure that visible documents show their diagnostics before all other documents.
|
||||
*
|
||||
* This improves the responsiveness in large workspaces as users usually don't care about diagnostics
|
||||
* in files that are currently not opened in the editor.
|
||||
*/
|
||||
protected sortDocuments(documents: LangiumDocument[]): LangiumDocument[] {
|
||||
let left = 0;
|
||||
let right = documents.length - 1;
|
||||
|
||||
while (left < right) {
|
||||
while (left < documents.length && this.hasTextDocument(documents[left])) {
|
||||
left++;
|
||||
}
|
||||
|
||||
while (right >= 0 && !this.hasTextDocument(documents[right])) {
|
||||
right--;
|
||||
}
|
||||
|
||||
if (left < right) {
|
||||
[documents[left], documents[right]] = [documents[right], documents[left]];
|
||||
}
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
private hasTextDocument(doc: LangiumDocument): boolean {
|
||||
return Boolean(this.textDocuments?.get(doc.uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given document should be relinked after changes were found in the given URIs.
|
||||
*/
|
||||
protected shouldRelink(document: LangiumDocument, changedUris: Set<string>): boolean {
|
||||
// Relink documents with linking errors -- maybe those references can be resolved now
|
||||
if (document.references.some(ref => ref.error !== undefined)) {
|
||||
return true;
|
||||
}
|
||||
// Check whether the document is affected by any of the changed URIs
|
||||
return this.indexManager.isAffected(document, changedUris);
|
||||
}
|
||||
|
||||
onUpdate(callback: DocumentUpdateListener): Disposable {
|
||||
this.updateListeners.push(callback);
|
||||
return Disposable.create(() => {
|
||||
const index = this.updateListeners.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
this.updateListeners.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetToState<T extends AstNode>(document: LangiumDocument<T>, state: DocumentState): void {
|
||||
switch (state) {
|
||||
case DocumentState.Changed: {
|
||||
// Fall through
|
||||
}
|
||||
case DocumentState.Parsed:
|
||||
this.indexManager.removeContent(document.uri);
|
||||
// Fall through
|
||||
case DocumentState.IndexedContent:
|
||||
document.localSymbols = undefined;
|
||||
// Fall through
|
||||
case DocumentState.ComputedScopes: {
|
||||
const linker = this.serviceRegistry.getServices(document.uri).references.Linker;
|
||||
linker.unlink(document);
|
||||
// Fall through
|
||||
}
|
||||
case DocumentState.Linked:
|
||||
this.indexManager.removeReferences(document.uri);
|
||||
// Fall through
|
||||
case DocumentState.IndexedReferences:
|
||||
document.diagnostics = undefined;
|
||||
this.buildState.delete(document.uri.toString());
|
||||
// Fall through
|
||||
case DocumentState.Validated:
|
||||
// do nothing and keep the buildState
|
||||
}
|
||||
if (document.state > state) {
|
||||
document.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
protected cleanUpDeleted<T extends AstNode>(document: LangiumDocument<T>): void {
|
||||
this.buildState.delete(document.uri.toString());
|
||||
this.indexManager.remove(document.uri);
|
||||
// Since this method `cleanUpDeleted` is not available from outside, the following line is not necessary, since the state is already set before.
|
||||
// This line does not hurt and makes the code to be in sync with `resetToState`.
|
||||
// If `cleanUpDeleted` is called in custom document builders at some more places, this line becomes necessary.
|
||||
document.state = DocumentState.Changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the given documents by stepping through all build phases. If a document's state indicates
|
||||
* that a certain build phase is already done, the phase is skipped for that document.
|
||||
*
|
||||
* @param documents The documents to build.
|
||||
* @param options the {@link BuildOptions} to use.
|
||||
* @param cancelToken A cancellation token that can be used to cancel the build.
|
||||
* @returns A promise that resolves when the build is done.
|
||||
*/
|
||||
protected async buildDocuments(documents: LangiumDocument[], options: BuildOptions, cancelToken: CancellationToken): Promise<void> {
|
||||
this.prepareBuild(documents, options);
|
||||
// 0. Parse content
|
||||
await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc =>
|
||||
this.langiumDocumentFactory.update(doc, cancelToken)
|
||||
);
|
||||
// 1. Index content: collect the documents' symbols being accessible by other documents
|
||||
await this.runCancelable(documents, DocumentState.IndexedContent, cancelToken, doc =>
|
||||
this.indexManager.updateContent(doc, cancelToken)
|
||||
);
|
||||
// 2. Local symbols: collect each documents' symbols being accessible within the document (only)
|
||||
await this.runCancelable(documents, DocumentState.ComputedScopes, cancelToken, async doc => {
|
||||
const scopeComputation = this.serviceRegistry.getServices(doc.uri).references.ScopeComputation;
|
||||
doc.localSymbols = await scopeComputation.collectLocalSymbols(doc, cancelToken);
|
||||
});
|
||||
// 3. Linking
|
||||
const toBeLinked = documents.filter(doc => this.shouldLink(doc));
|
||||
await this.runCancelable(toBeLinked, DocumentState.Linked, cancelToken, doc => {
|
||||
const linker = this.serviceRegistry.getServices(doc.uri).references.Linker;
|
||||
return linker.link(doc, cancelToken);
|
||||
});
|
||||
// 4. Index references
|
||||
await this.runCancelable(toBeLinked, DocumentState.IndexedReferences, cancelToken, doc =>
|
||||
this.indexManager.updateReferences(doc, cancelToken)
|
||||
);
|
||||
// 5. Validation
|
||||
const toBeValidated = documents.filter(doc => {
|
||||
if (this.shouldValidate(doc)) {
|
||||
return true; // the build state is marked as completed after finishing the validation for the current document
|
||||
} else {
|
||||
this.markAsCompleted(doc); // since the validation is skipped for this document, it is already completed now
|
||||
return false;
|
||||
}
|
||||
});
|
||||
await this.runCancelable(toBeValidated, DocumentState.Validated, cancelToken, async doc => {
|
||||
await this.validate(doc, cancelToken);
|
||||
this.markAsCompleted(doc);
|
||||
});
|
||||
}
|
||||
|
||||
protected markAsCompleted(document: LangiumDocument): void {
|
||||
const state = this.buildState.get(document.uri.toString());
|
||||
if (state) {
|
||||
state.completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs prior to beginning the build process to update the {@link DocumentBuildState} for each document
|
||||
*
|
||||
* @param documents collection of documents to be built
|
||||
* @param options the {@link BuildOptions} to use
|
||||
*/
|
||||
protected prepareBuild(documents: LangiumDocument[], options: BuildOptions): void {
|
||||
for (const doc of documents) {
|
||||
const key = doc.uri.toString();
|
||||
const state = this.buildState.get(key);
|
||||
if (
|
||||
!state // If the document has no previous build state, we set it.
|
||||
|| state.completed // If it has one, but it's already marked as completed, we overwrite it.
|
||||
) {
|
||||
this.buildState.set(key, {
|
||||
completed: false,
|
||||
options,
|
||||
result: state?.result
|
||||
});
|
||||
} else {
|
||||
// If the previous build was not completed, we keep its DocumentState and continue from the DocumentState where it was cancelled,
|
||||
// e.g. the previous build options are used, including the previously requested validation categories.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a cancelable operation on a set of documents to bring them to a specified {@link DocumentState}.
|
||||
*
|
||||
* @param documents The array of documents to process.
|
||||
* @param targetState The target {@link DocumentState} to bring the documents to.
|
||||
* @param cancelToken A token that can be used to cancel the operation.
|
||||
* @param callback A function to be called for each document.
|
||||
* @returns A promise that resolves when all documents have been processed or the operation is canceled.
|
||||
* @throws Will throw `OperationCancelled` if the operation is canceled via a `CancellationToken`.
|
||||
*/
|
||||
protected async runCancelable(documents: LangiumDocument[], targetState: DocumentState, cancelToken: CancellationToken,
|
||||
callback: (document: LangiumDocument) => MaybePromise<unknown>): Promise<void> {
|
||||
for (const document of documents) {
|
||||
if (document.state < targetState) {
|
||||
await interruptAndCheck(cancelToken);
|
||||
await callback(document);
|
||||
document.state = targetState;
|
||||
await this.notifyDocumentPhase(document, targetState, cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not use `filtered` here, as that will miss documents that have previously reached the current target state.
|
||||
// For example, this happens in case the cancellation triggers between the processing of two documents
|
||||
// or files that were picked up during the workspace initialization.
|
||||
const targetStateDocs = documents.filter(doc => doc.state === targetState);
|
||||
await this.notifyBuildPhase(targetStateDocs, targetState, cancelToken);
|
||||
this.currentState = targetState;
|
||||
}
|
||||
|
||||
onBuildPhase(targetState: DocumentState, callback: DocumentBuildListener): Disposable {
|
||||
this.buildPhaseListeners.add(targetState, callback);
|
||||
return Disposable.create(() => {
|
||||
this.buildPhaseListeners.delete(targetState, callback);
|
||||
});
|
||||
}
|
||||
|
||||
onDocumentPhase(targetState: DocumentState, callback: DocumentPhaseListener): Disposable {
|
||||
this.documentPhaseListeners.add(targetState, callback);
|
||||
return Disposable.create(() => {
|
||||
this.documentPhaseListeners.delete(targetState, callback);
|
||||
});
|
||||
}
|
||||
|
||||
waitUntil(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
|
||||
waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken): Promise<URI>;
|
||||
waitUntil(state: DocumentState, uriOrToken?: URI | CancellationToken, cancelToken?: CancellationToken): Promise<URI | void> {
|
||||
let uri: URI | undefined = undefined;
|
||||
if (uriOrToken && 'path' in uriOrToken) {
|
||||
uri = uriOrToken;
|
||||
} else {
|
||||
cancelToken = uriOrToken;
|
||||
}
|
||||
cancelToken ??= CancellationToken.None;
|
||||
if (uri) {
|
||||
return this.awaitDocumentState(state, uri, cancelToken);
|
||||
|
||||
} else {
|
||||
return this.awaitBuilderState(state, cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
protected awaitDocumentState(state: DocumentState, uri: URI, cancelToken: CancellationToken): Promise<URI> {
|
||||
const document = this.langiumDocuments.getDocument(uri);
|
||||
if (!document) {
|
||||
return Promise.reject(
|
||||
new ResponseError(
|
||||
LSPErrorCodes.ServerCancelled,
|
||||
`No document found for URI: ${uri.toString()}`
|
||||
)
|
||||
);
|
||||
|
||||
} else if (document.state >= state) {
|
||||
return Promise.resolve(uri);
|
||||
|
||||
} else if (cancelToken.isCancellationRequested) {
|
||||
return Promise.reject(OperationCancelled);
|
||||
|
||||
} else if (this.currentState >= state && state > document.state) {
|
||||
// this would imply that the document has been excluded from linking or validation, for example;
|
||||
// this should never occur, the LS need to make sure that the affected document is properly built,
|
||||
// alternatively, the build state requirement need to be relaxed.
|
||||
return Promise.reject(
|
||||
new ResponseError(
|
||||
LSPErrorCodes.RequestFailed,
|
||||
`Document state of ${uri.toString()} is ${DocumentState[document.state]}, requiring ${DocumentState[state]}, but workspace state is already ${DocumentState[this.currentState]}. Returning undefined.`
|
||||
)
|
||||
);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const buildDisposable = this.onDocumentPhase(state, (doc) => {
|
||||
if (UriUtils.equals(doc.uri, uri)) {
|
||||
buildDisposable.dispose();
|
||||
cancelDisposable.dispose();
|
||||
resolve(doc.uri);
|
||||
}
|
||||
});
|
||||
const cancelDisposable = cancelToken!.onCancellationRequested(() => {
|
||||
buildDisposable.dispose();
|
||||
cancelDisposable.dispose();
|
||||
reject(OperationCancelled);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected awaitBuilderState(state: DocumentState, cancelToken: CancellationToken): Promise<void> {
|
||||
if (this.currentState >= state) {
|
||||
return Promise.resolve();
|
||||
} else if (cancelToken.isCancellationRequested) {
|
||||
return Promise.reject(OperationCancelled);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const buildDisposable = this.onBuildPhase(state, () => {
|
||||
buildDisposable.dispose();
|
||||
cancelDisposable.dispose();
|
||||
resolve();
|
||||
});
|
||||
const cancelDisposable = cancelToken!.onCancellationRequested(() => {
|
||||
buildDisposable.dispose();
|
||||
cancelDisposable.dispose();
|
||||
reject(OperationCancelled);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected async notifyDocumentPhase(document: LangiumDocument, state: DocumentState, cancelToken: CancellationToken): Promise<void> {
|
||||
const listeners = this.documentPhaseListeners.get(state);
|
||||
const listenersCopy = listeners.slice();
|
||||
for (const listener of listenersCopy) {
|
||||
try {
|
||||
await interruptAndCheck(cancelToken);
|
||||
await listener(document, cancelToken);
|
||||
} catch (err) {
|
||||
// Ignore cancellation errors
|
||||
// We want to finish the listeners before throwing
|
||||
if (!isOperationCancelled(err)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async notifyBuildPhase(documents: LangiumDocument[], state: DocumentState, cancelToken: CancellationToken): Promise<void> {
|
||||
if (documents.length === 0) {
|
||||
// Don't notify when no document has been processed
|
||||
return;
|
||||
}
|
||||
const listeners = this.buildPhaseListeners.get(state);
|
||||
const listenersCopy = listeners.slice();
|
||||
for (const listener of listenersCopy) {
|
||||
await interruptAndCheck(cancelToken);
|
||||
await listener(documents, cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given document should be linked during a build. The default
|
||||
* implementation checks the `eagerLinking` property of the build options. If it's set to `true`
|
||||
* or `undefined`, the document is included in the linking phase. This also affects the
|
||||
* references indexing phase, which depends on eager linking.
|
||||
*/
|
||||
protected shouldLink(document: LangiumDocument): boolean {
|
||||
return this.getBuildOptions(document).eagerLinking ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given document should be validated during a build. The default
|
||||
* implementation checks the `validation` property of the build options. If it's set to `true`
|
||||
* or a `ValidationOptions` object, the document is included in the validation phase.
|
||||
*/
|
||||
protected shouldValidate(document: LangiumDocument): boolean {
|
||||
return Boolean(this.getBuildOptions(document).validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run validation checks on the given document and store the resulting diagnostics in the document.
|
||||
* If the document already contains diagnostics, the new ones are added to the list.
|
||||
*/
|
||||
protected async validate(document: LangiumDocument, cancelToken: CancellationToken): Promise<void> {
|
||||
const validator = this.serviceRegistry.getServices(document.uri).validation.DocumentValidator;
|
||||
const options = this.getBuildOptions(document);
|
||||
const validationOptions = typeof options.validation === 'object' ? { ...options.validation } : {};
|
||||
validationOptions.categories = this.findMissingValidationCategories(document, options); // execute only not-yet-executed categories
|
||||
const diagnostics = await validator.validateDocument(document, validationOptions, cancelToken);
|
||||
if (document.diagnostics) {
|
||||
document.diagnostics.push(...diagnostics); // keep diagnostics of previously executed categories
|
||||
} else {
|
||||
document.diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
// Store information about the executed validation in the build state
|
||||
const state = this.buildState.get(document.uri.toString());
|
||||
if (state) {
|
||||
state.result ??= {};
|
||||
if (state.result.validationChecks) {
|
||||
state.result.validationChecks = stream(state.result.validationChecks).concat(validationOptions.categories).distinct().toArray();
|
||||
} else {
|
||||
state.result.validationChecks = [...validationOptions.categories];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getBuildOptions(document: LangiumDocument): BuildOptions {
|
||||
return this.buildState.get(document.uri.toString())?.options ?? {};
|
||||
}
|
||||
|
||||
}
|
||||
120
frontend/node_modules/langium/src/workspace/file-system-provider.ts
generated
vendored
Normal file
120
frontend/node_modules/langium/src/workspace/file-system-provider.ts
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/******************************************************************************
|
||||
* Copyright 2022 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';
|
||||
|
||||
export interface FileSystemNode {
|
||||
readonly isFile: boolean;
|
||||
readonly isDirectory: boolean;
|
||||
readonly uri: URI;
|
||||
}
|
||||
|
||||
export type FileSystemFilter = (node: FileSystemNode) => boolean;
|
||||
|
||||
/**
|
||||
* Provides methods to interact with an abstract file system. The default implementation is based on the node.js `fs` API.
|
||||
*/
|
||||
export interface FileSystemProvider {
|
||||
/**
|
||||
* Gets the status of a file or directory.
|
||||
* The status includes meta data such as whether the node is a file or directory.
|
||||
* @param uri The URI of the file or directory.
|
||||
*/
|
||||
stat(uri: URI): Promise<FileSystemNode>;
|
||||
/**
|
||||
* Gets the status of a file or directory synchronously.
|
||||
* The status includes meta data such as whether the node is a file or directory.
|
||||
* @param uri The URI of the file or directory.
|
||||
*/
|
||||
statSync(uri: URI): FileSystemNode;
|
||||
/**
|
||||
* Checks if a file exists at the specified URI.
|
||||
* @returns `true` if a file exists at the specified URI, `false` otherwise.
|
||||
*/
|
||||
exists(uri: URI): Promise<boolean>;
|
||||
/**
|
||||
* Checks if a file exists at the specified URI synchronously.
|
||||
* @returns `true` if a file exists at the specified URI, `false` otherwise.
|
||||
*/
|
||||
existsSync(uri: URI): boolean;
|
||||
/**
|
||||
* Reads a binary file asynchronously from a given URI.
|
||||
* @returns The binary content of the file with the specified URI.
|
||||
*/
|
||||
readBinary(uri: URI): Promise<Uint8Array>;
|
||||
/**
|
||||
* Reads a binary file synchronously from a given URI.
|
||||
* @returns The binary content of the file with the specified URI.
|
||||
*/
|
||||
readBinarySync(uri: URI): Uint8Array;
|
||||
/**
|
||||
* Reads a document asynchronously from a given URI.
|
||||
* @returns The string content of the file with the specified URI.
|
||||
*/
|
||||
readFile(uri: URI): Promise<string>;
|
||||
/**
|
||||
* Reads a document synchronously from a given URI.
|
||||
* @returns The string content of the file with the specified
|
||||
*/
|
||||
readFileSync(uri: URI): string;
|
||||
/**
|
||||
* Reads the directory information for the given URI.
|
||||
* @returns The list of file system entries that are contained within the specified directory.
|
||||
*/
|
||||
readDirectory(uri: URI): Promise<FileSystemNode[]>;
|
||||
/**
|
||||
* Reads the directory information for the given URI synchronously.
|
||||
* @returns The list of file system entries that are contained within the specified directory.
|
||||
*/
|
||||
readDirectorySync(uri: URI): FileSystemNode[];
|
||||
}
|
||||
|
||||
export class EmptyFileSystemProvider implements FileSystemProvider {
|
||||
|
||||
stat(_uri: URI): Promise<FileSystemNode> {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
|
||||
statSync(_uri: URI): FileSystemNode {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
async exists(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
existsSync(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
readBinary(): Promise<Uint8Array> {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
|
||||
readBinarySync(): Uint8Array {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
|
||||
readFile(): Promise<string> {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
|
||||
readFileSync(): string {
|
||||
throw new Error('No file system is available.');
|
||||
}
|
||||
|
||||
async readDirectory(): Promise<FileSystemNode[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
readDirectorySync(): FileSystemNode[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const EmptyFileSystem = {
|
||||
fileSystemProvider: () => new EmptyFileSystemProvider()
|
||||
};
|
||||
195
frontend/node_modules/langium/src/workspace/index-manager.ts
generated
vendored
Normal file
195
frontend/node_modules/langium/src/workspace/index-manager.ts
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
/******************************************************************************
|
||||
* 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 { ServiceRegistry } from '../service-registry.js';
|
||||
import type { LangiumSharedCoreServices } from '../services.js';
|
||||
import type { AstNode, AstNodeDescription, AstReflection } from '../syntax-tree.js';
|
||||
import { getDocument } from '../utils/ast-utils.js';
|
||||
import { ContextCache } from '../utils/caching.js';
|
||||
import { CancellationToken } from '../utils/cancellation.js';
|
||||
import type { Stream } from '../utils/stream.js';
|
||||
import { stream } from '../utils/stream.js';
|
||||
import type { URI } from '../utils/uri-utils.js';
|
||||
import { UriUtils } from '../utils/uri-utils.js';
|
||||
import type { ReferenceDescription } from './ast-descriptions.js';
|
||||
import type { LangiumDocument, LangiumDocuments } from './documents.js';
|
||||
|
||||
/**
|
||||
* The index manager is responsible for keeping metadata about symbols and cross-references
|
||||
* in the workspace. It is used to look up symbols in the global scope, mostly during linking
|
||||
* and completion. This service is shared between all languages of a language server.
|
||||
*/
|
||||
export interface IndexManager {
|
||||
|
||||
/**
|
||||
* Remove the specified document URI from the index.
|
||||
* Necessary when documents are deleted and not referenceable anymore.
|
||||
*
|
||||
* @param uri The URI of the document for which index data shall be removed
|
||||
*/
|
||||
remove(uri: URI): void;
|
||||
|
||||
/**
|
||||
* Remove only the information about the exportable content of a document.
|
||||
*/
|
||||
removeContent(uri: URI): void;
|
||||
|
||||
/**
|
||||
* Remove only the information about the cross-references of a document.
|
||||
*/
|
||||
removeReferences(uri: URI): void;
|
||||
|
||||
/**
|
||||
* Update the information about the exportable content of a document inside the index.
|
||||
*
|
||||
* @param document Document to be updated
|
||||
* @param cancelToken Indicates when to cancel the current operation.
|
||||
* @throws `OperationCanceled` if a user action occurs during execution
|
||||
*/
|
||||
updateContent(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update the information about the cross-references of a document inside the index.
|
||||
*
|
||||
* @param document Document to be updated
|
||||
* @param cancelToken Indicates when to cancel the current operation.
|
||||
* @throws `OperationCanceled` if a user action occurs during execution
|
||||
*/
|
||||
updateReferences(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void>;
|
||||
|
||||
/**
|
||||
* Determine whether the given document could be affected by changes of the documents
|
||||
* identified by the given URIs (second parameter). The document is typically regarded as
|
||||
* affected if it contains a reference to any of the changed files.
|
||||
*
|
||||
* @param document Document to check whether it's affected
|
||||
* @param changedUris URIs of the changed documents
|
||||
*/
|
||||
isAffected(document: LangiumDocument, changedUris: Set<string>): boolean;
|
||||
|
||||
/**
|
||||
* Compute a list of all exported elements, optionally filtered using a type identifier and document URIs.
|
||||
*
|
||||
* @param nodeType The type to filter with, or `undefined` to return descriptions of all types.
|
||||
* @param uris If specified, only returns elements from the given URIs.
|
||||
* @returns a `Stream` containing all globally visible nodes (of a given type).
|
||||
*/
|
||||
allElements(nodeType?: string, uris?: Set<string>): Stream<AstNodeDescription>;
|
||||
|
||||
/**
|
||||
* Returns all known references that are pointing to the given `targetNode`.
|
||||
*
|
||||
* @param targetNode the `AstNode` to look up references for
|
||||
* @param astNodePath the path that points to the `targetNode` inside the document. See also `AstNodeLocator`
|
||||
*
|
||||
* @returns a `Stream` of references that are targeting the `targetNode`
|
||||
*/
|
||||
findAllReferences(targetNode: AstNode, astNodePath: string): Stream<ReferenceDescription>;
|
||||
|
||||
}
|
||||
|
||||
export class DefaultIndexManager implements IndexManager {
|
||||
|
||||
protected readonly serviceRegistry: ServiceRegistry;
|
||||
protected readonly documents: LangiumDocuments;
|
||||
protected readonly astReflection: AstReflection;
|
||||
|
||||
/**
|
||||
* The symbol index stores all `AstNodeDescription` items exported by a document.
|
||||
* The key used in this map is the string representation of the specific document URI.
|
||||
*/
|
||||
protected readonly symbolIndex = new Map<string, AstNodeDescription[]>();
|
||||
/**
|
||||
* This is a cache for the `allElements()` method.
|
||||
* It caches the descriptions from `symbolIndex` grouped by types.
|
||||
*/
|
||||
protected readonly symbolByTypeIndex = new ContextCache<string, string, AstNodeDescription[]>();
|
||||
/**
|
||||
* This index keeps track of all `ReferenceDescription` items exported by a document.
|
||||
* This is used to compute which elements are affected by a document change
|
||||
* and for finding references to an AST node.
|
||||
*/
|
||||
protected readonly referenceIndex = new Map<string, ReferenceDescription[]>();
|
||||
|
||||
constructor(services: LangiumSharedCoreServices) {
|
||||
this.documents = services.workspace.LangiumDocuments;
|
||||
this.serviceRegistry = services.ServiceRegistry;
|
||||
this.astReflection = services.AstReflection;
|
||||
}
|
||||
|
||||
findAllReferences(targetNode: AstNode, astNodePath: string): Stream<ReferenceDescription> {
|
||||
const targetDocUri = getDocument(targetNode).uri;
|
||||
const result: ReferenceDescription[] = [];
|
||||
this.referenceIndex.forEach(docRefs => {
|
||||
docRefs.forEach(refDescr => {
|
||||
if (UriUtils.equals(refDescr.targetUri, targetDocUri) && refDescr.targetPath === astNodePath) {
|
||||
result.push(refDescr);
|
||||
}
|
||||
});
|
||||
});
|
||||
return stream(result);
|
||||
}
|
||||
|
||||
allElements(nodeType?: string, uris?: Set<string>): Stream<AstNodeDescription> {
|
||||
let documentUris = stream(this.symbolIndex.keys());
|
||||
if (uris) {
|
||||
documentUris = documentUris.filter(uri => !uris || uris.has(uri));
|
||||
}
|
||||
return documentUris
|
||||
.map(uri => this.getFileDescriptions(uri, nodeType))
|
||||
.flat();
|
||||
}
|
||||
|
||||
protected getFileDescriptions(uri: string, nodeType?: string): AstNodeDescription[] {
|
||||
if (!nodeType) {
|
||||
return this.symbolIndex.get(uri) ?? [];
|
||||
}
|
||||
const descriptions = this.symbolByTypeIndex.get(uri, nodeType, () => {
|
||||
const allFileDescriptions = this.symbolIndex.get(uri) ?? [];
|
||||
return allFileDescriptions.filter(e => this.astReflection.isSubtype(e.type, nodeType));
|
||||
});
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
remove(uri: URI): void {
|
||||
this.removeContent(uri);
|
||||
this.removeReferences(uri);
|
||||
}
|
||||
|
||||
removeContent(uri: URI): void {
|
||||
const uriString = uri.toString();
|
||||
this.symbolIndex.delete(uriString);
|
||||
this.symbolByTypeIndex.clear(uriString);
|
||||
}
|
||||
|
||||
removeReferences(uri: URI): void {
|
||||
const uriString = uri.toString();
|
||||
this.referenceIndex.delete(uriString);
|
||||
}
|
||||
|
||||
async updateContent(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
|
||||
const services = this.serviceRegistry.getServices(document.uri);
|
||||
const exports = await services.references.ScopeComputation.collectExportedSymbols(document, cancelToken);
|
||||
const uri = document.uri.toString();
|
||||
this.symbolIndex.set(uri, exports);
|
||||
this.symbolByTypeIndex.clear(uri);
|
||||
}
|
||||
|
||||
async updateReferences(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
|
||||
const services = this.serviceRegistry.getServices(document.uri);
|
||||
const indexData = await services.workspace.ReferenceDescriptionProvider.createDescriptions(document, cancelToken);
|
||||
this.referenceIndex.set(document.uri.toString(), indexData);
|
||||
}
|
||||
|
||||
isAffected(document: LangiumDocument, changedUris: Set<string>): boolean {
|
||||
const references = this.referenceIndex.get(document.uri.toString());
|
||||
if (!references) {
|
||||
return false;
|
||||
}
|
||||
return references.some(ref => !ref.local && changedUris.has(ref.targetUri.toString()));
|
||||
}
|
||||
|
||||
}
|
||||
114
frontend/node_modules/langium/src/workspace/workspace-lock.ts
generated
vendored
Normal file
114
frontend/node_modules/langium/src/workspace/workspace-lock.ts
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/******************************************************************************
|
||||
* 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 AbstractCancellationTokenSource, CancellationToken, CancellationTokenSource } from '../utils/cancellation.js';
|
||||
import { Deferred, isOperationCancelled, startCancelableOperation, type MaybePromise } from '../utils/promise-utils.js';
|
||||
|
||||
/**
|
||||
* Utility service to execute mutually exclusive actions.
|
||||
*/
|
||||
export interface WorkspaceLock {
|
||||
/**
|
||||
* Performs a single async action, like initializing the workspace or processing document changes.
|
||||
* Only one action will be executed at a time.
|
||||
*
|
||||
* When another action is queued up, the token provided for the action will be cancelled.
|
||||
* Assuming the action makes use of this token, the next action only has to wait for the current action to finish cancellation.
|
||||
*/
|
||||
write(action: (token: CancellationToken) => MaybePromise<void>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Performs a single action, like computing completion results or providing workspace symbols.
|
||||
* Read actions will only be executed after all write actions have finished. They will be executed in parallel if possible.
|
||||
*
|
||||
* If a write action is currently running, the read action will be queued up and executed afterwards.
|
||||
* If a new write action is queued up while a read action is waiting, the write action will receive priority and will be handled before the read action.
|
||||
*
|
||||
* Note that read actions are not allowed to modify anything in the workspace. Please use {@link write} instead.
|
||||
*/
|
||||
read<T>(action: () => MaybePromise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Cancels the last queued write action. All previous write actions already have been cancelled.
|
||||
*/
|
||||
cancelWrite(): void;
|
||||
}
|
||||
|
||||
type LockAction<T = void> = (token: CancellationToken) => MaybePromise<T>;
|
||||
|
||||
interface LockEntry {
|
||||
action: LockAction<unknown>;
|
||||
deferred: Deferred<unknown>;
|
||||
cancellationToken: CancellationToken;
|
||||
}
|
||||
|
||||
export class DefaultWorkspaceLock implements WorkspaceLock {
|
||||
|
||||
private previousTokenSource: AbstractCancellationTokenSource = new CancellationTokenSource();
|
||||
private writeQueue: LockEntry[] = [];
|
||||
private readQueue: LockEntry[] = [];
|
||||
private done = true;
|
||||
|
||||
write(action: (token: CancellationToken) => MaybePromise<void>): Promise<void> {
|
||||
this.cancelWrite();
|
||||
const tokenSource = startCancelableOperation();
|
||||
this.previousTokenSource = tokenSource;
|
||||
return this.enqueue(this.writeQueue, action, tokenSource.token);
|
||||
}
|
||||
|
||||
read<T>(action: () => MaybePromise<T>): Promise<T> {
|
||||
return this.enqueue(this.readQueue, action);
|
||||
}
|
||||
|
||||
private enqueue<T = void>(queue: LockEntry[], action: LockAction<T>, cancellationToken = CancellationToken.None): Promise<T> {
|
||||
const deferred = new Deferred<unknown>();
|
||||
const entry: LockEntry = {
|
||||
action,
|
||||
deferred,
|
||||
cancellationToken
|
||||
};
|
||||
queue.push(entry);
|
||||
this.performNextOperation();
|
||||
return deferred.promise as Promise<T>;
|
||||
}
|
||||
|
||||
private async performNextOperation(): Promise<void> {
|
||||
if (!this.done) {
|
||||
return;
|
||||
}
|
||||
const entries: LockEntry[] = [];
|
||||
if (this.writeQueue.length > 0) {
|
||||
// Just perform the next write action
|
||||
entries.push(this.writeQueue.shift()!);
|
||||
} else if (this.readQueue.length > 0) {
|
||||
// Empty the read queue and perform all actions in parallel
|
||||
entries.push(...this.readQueue.splice(0, this.readQueue.length));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.done = false;
|
||||
await Promise.all(entries.map(async ({ action, deferred, cancellationToken }) => {
|
||||
try {
|
||||
// Move the execution of the action to the next event loop tick via `Promise.resolve()`
|
||||
const result = await Promise.resolve().then(() => action(cancellationToken));
|
||||
deferred.resolve(result);
|
||||
} catch (err) {
|
||||
if (isOperationCancelled(err)) {
|
||||
// If the operation was cancelled, we don't want to reject the promise
|
||||
deferred.resolve(undefined);
|
||||
} else {
|
||||
deferred.reject(err);
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.done = true;
|
||||
this.performNextOperation();
|
||||
}
|
||||
|
||||
cancelWrite(): void {
|
||||
this.previousTokenSource.cancel();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user