From a140a64fb9254f49d6398941d41dd8065d69cb67 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 2 Sep 2024 15:11:35 +0200 Subject: [PATCH 1/4] Normalize all LSP URIs --- .../langium/src/lsp/default-lsp-module.ts | 5 +- packages/langium/src/lsp/index.ts | 1 + packages/langium/src/lsp/lsp-services.ts | 3 +- .../src/lsp/normalized-text-documents.ts | 217 ++++++++++++++++++ packages/langium/src/utils/uri-utils.ts | 4 + packages/langium/test/utils/uri-utils.test.ts | 20 ++ 6 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 packages/langium/src/lsp/normalized-text-documents.ts diff --git a/packages/langium/src/lsp/default-lsp-module.ts b/packages/langium/src/lsp/default-lsp-module.ts index b5e1a3326..fa972ef25 100644 --- a/packages/langium/src/lsp/default-lsp-module.ts +++ b/packages/langium/src/lsp/default-lsp-module.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { type Connection, TextDocuments } from 'vscode-languageserver'; +import type { Connection } from 'vscode-languageserver'; import { createDefaultCoreModule, createDefaultSharedCoreModule, type DefaultCoreModuleContext, type DefaultSharedCoreModuleContext } from '../default-module.js'; import { Module } from '../dependency-injection.js'; import type { LangiumDefaultCoreServices, LangiumDefaultSharedCoreServices } from '../services.js'; @@ -23,6 +23,7 @@ import { DefaultNodeKindProvider } from './node-kind-provider.js'; import { DefaultReferencesProvider } from './references-provider.js'; import { DefaultRenameProvider } from './rename-provider.js'; import { DefaultWorkspaceSymbolProvider } from './workspace-symbol-provider.js'; +import { NormalizedTextDocuments } from './normalized-text-documents.js'; /** * Context required for creating the default language-specific dependency injection module. @@ -95,7 +96,7 @@ export function createDefaultSharedLSPModule(context: DefaultSharedModuleContext FuzzyMatcher: () => new DefaultFuzzyMatcher(), }, workspace: { - TextDocuments: () => new TextDocuments(TextDocument) + TextDocuments: () => new NormalizedTextDocuments(TextDocument) } }; } diff --git a/packages/langium/src/lsp/index.ts b/packages/langium/src/lsp/index.ts index bd5698128..34b746669 100644 --- a/packages/langium/src/lsp/index.ts +++ b/packages/langium/src/lsp/index.ts @@ -27,6 +27,7 @@ export * from './inlay-hint-provider.js'; export * from './language-server.js'; export * from './lsp-services.js'; export * from './node-kind-provider.js'; +export * from './normalized-text-documents.js'; export * from './references-provider.js'; export * from './rename-provider.js'; export * from './semantic-token-provider.js'; diff --git a/packages/langium/src/lsp/lsp-services.ts b/packages/langium/src/lsp/lsp-services.ts index 25d1929c9..19d904ac1 100644 --- a/packages/langium/src/lsp/lsp-services.ts +++ b/packages/langium/src/lsp/lsp-services.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Connection, TextDocuments } from 'vscode-languageserver'; +import type { Connection } from 'vscode-languageserver'; import type { DeepPartial, LangiumCoreServices, LangiumSharedCoreServices } from '../services.js'; import type { TextDocument } from '../workspace/documents.js'; import type { CallHierarchyProvider } from './call-hierarchy-provider.js'; @@ -34,6 +34,7 @@ import type { SignatureHelpProvider } from './signature-help-provider.js'; import type { TypeHierarchyProvider } from './type-hierarchy-provider.js'; import type { TypeDefinitionProvider } from './type-provider.js'; import type { WorkspaceSymbolProvider } from './workspace-symbol-provider.js'; +import type { TextDocuments } from './normalized-text-documents.js'; /** * Combined Core + LSP services of Langium (total services) diff --git a/packages/langium/src/lsp/normalized-text-documents.ts b/packages/langium/src/lsp/normalized-text-documents.ts new file mode 100644 index 000000000..88465bc92 --- /dev/null +++ b/packages/langium/src/lsp/normalized-text-documents.ts @@ -0,0 +1,217 @@ +/****************************************************************************** + * Copyright 2024 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 { + Connection, DidOpenTextDocumentParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentsConfiguration, TextDocumentChangeEvent, + TextDocumentWillSaveEvent, RequestHandler, TextEdit, Event, WillSaveTextDocumentParams, CancellationToken, DidSaveTextDocumentParams +} from 'vscode-languageserver'; +import { TextDocumentSyncKind, Disposable, Emitter } from 'vscode-languageserver'; +import { UriUtils } from '../utils/uri-utils.js'; + +export interface TextDocuments { + /** + * An event that fires when a text document managed by this manager + * has been opened. + */ + readonly onDidOpen: Event>; + /** + * An event that fires when a text document managed by this manager + * has been opened or the content changes. + */ + readonly onDidChangeContent: Event>; + /** + * An event that fires when a text document managed by this manager + * will be saved. + */ + readonly onWillSave: Event>; + /** + * Sets a handler that will be called if a participant wants to provide + * edits during a text document save. + */ + onWillSaveWaitUntil(handler: RequestHandler, TextEdit[], void>): void; + /** + * An event that fires when a text document managed by this manager + * has been saved. + */ + readonly onDidSave: Event>; + /** + * An event that fires when a text document managed by this manager + * has been closed. + */ + readonly onDidClose: Event>; + /** + * Returns the document for the given URI. Returns undefined if + * the document is not managed by this instance. + * + * @param uri The text document's URI to retrieve. + * @return the text document or `undefined`. + */ + get(uri: string): T | undefined; + /** + * Returns all text documents managed by this instance. + * + * @return all text documents. + */ + all(): T[]; + /** + * Returns the URIs of all text documents managed by this instance. + * + * @return the URI's of all text documents. + */ + keys(): string[]; + /** + * Listens for `low level` notification on the given connection to + * update the text documents managed by this instance. + * + * Please note that the connection only provides handlers not an event model. Therefore + * listening on a connection will overwrite the following handlers on a connection: + * `onDidOpenTextDocument`, `onDidChangeTextDocument`, `onDidCloseTextDocument`, + * `onWillSaveTextDocument`, `onWillSaveTextDocumentWaitUntil` and `onDidSaveTextDocument`. + * + * Use the corresponding events on the TextDocuments instance instead. + * + * @param connection The connection to listen on. + */ + listen(connection: Connection): Disposable; +} + +// Adapted from: +// https://github.com/microsoft/vscode-languageserver-node/blob/8f5fa710d3a9f60ff5e7583a9e61b19f86e39da3/server/src/common/textDocuments.ts + +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +/** + * Normalizing text document manager. Normalizes all incoming URIs to the same format used by VS Code. + */ +export class NormalizedTextDocuments implements TextDocuments { + + private readonly _configuration: TextDocumentsConfiguration; + + private readonly _syncedDocuments: Map; + + private readonly _onDidChangeContent: Emitter>; + private readonly _onDidOpen: Emitter>; + private readonly _onDidClose: Emitter>; + private readonly _onDidSave: Emitter>; + private readonly _onWillSave: Emitter>; + private _willSaveWaitUntil: RequestHandler, TextEdit[], void> | undefined; + + public constructor(configuration: TextDocumentsConfiguration) { + this._configuration = configuration; + this._syncedDocuments = new Map(); + + this._onDidChangeContent = new Emitter>(); + this._onDidOpen = new Emitter>(); + this._onDidClose = new Emitter>(); + this._onDidSave = new Emitter>(); + this._onWillSave = new Emitter>(); + } + + public get onDidOpen(): Event> { + return this._onDidOpen.event; + } + + public get onDidChangeContent(): Event> { + return this._onDidChangeContent.event; + } + + public get onWillSave(): Event> { + return this._onWillSave.event; + } + + public onWillSaveWaitUntil(handler: RequestHandler, TextEdit[], void>) { + this._willSaveWaitUntil = handler; + } + + public get onDidSave(): Event> { + return this._onDidSave.event; + } + + public get onDidClose(): Event> { + return this._onDidClose.event; + } + + public get(uri: string): T | undefined { + return this._syncedDocuments.get(UriUtils.normalize(uri)); + } + + public all(): T[] { + return Array.from(this._syncedDocuments.values()); + } + + public keys(): string[] { + return Array.from(this._syncedDocuments.keys()); + } + + public listen(connection: Connection): Disposable { + // Required for interoperability with the the vscode-languageserver package + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (connection as any).__textDocumentSync = TextDocumentSyncKind.Incremental; + const disposables: Disposable[] = []; + disposables.push(connection.onDidOpenTextDocument((event: DidOpenTextDocumentParams) => { + const td = event.textDocument; + const uri = UriUtils.normalize(td.uri); + const document = this._configuration.create(uri, td.languageId, td.version, td.text); + + this._syncedDocuments.set(uri, document); + const toFire = Object.freeze({ document }); + this._onDidOpen.fire(toFire); + this._onDidChangeContent.fire(toFire); + })); + disposables.push(connection.onDidChangeTextDocument((event: DidChangeTextDocumentParams) => { + const td = event.textDocument; + const changes = event.contentChanges; + if (changes.length === 0) { + return; + } + + const { version } = td; + if (version === null || version === undefined) { + throw new Error(`Received document change event for ${td.uri} without valid version identifier`); + } + const uri = UriUtils.normalize(td.uri); + + let syncedDocument = this._syncedDocuments.get(uri); + if (syncedDocument !== undefined) { + syncedDocument = this._configuration.update(syncedDocument, changes, version); + this._syncedDocuments.set(uri, syncedDocument); + this._onDidChangeContent.fire(Object.freeze({ document: syncedDocument })); + } + })); + disposables.push(connection.onDidCloseTextDocument((event: DidCloseTextDocumentParams) => { + const uri = UriUtils.normalize(event.textDocument.uri); + const syncedDocument = this._syncedDocuments.get(uri); + if (syncedDocument !== undefined) { + this._syncedDocuments.delete(uri); + this._onDidClose.fire(Object.freeze({ document: syncedDocument })); + } + })); + disposables.push(connection.onWillSaveTextDocument((event: WillSaveTextDocumentParams) => { + const syncedDocument = this._syncedDocuments.get(UriUtils.normalize(event.textDocument.uri)); + if (syncedDocument !== undefined) { + this._onWillSave.fire(Object.freeze({ document: syncedDocument, reason: event.reason })); + } + })); + disposables.push(connection.onWillSaveTextDocumentWaitUntil((event: WillSaveTextDocumentParams, token: CancellationToken) => { + const syncedDocument = this._syncedDocuments.get(UriUtils.normalize(event.textDocument.uri)); + if (syncedDocument !== undefined && this._willSaveWaitUntil) { + return this._willSaveWaitUntil(Object.freeze({ document: syncedDocument, reason: event.reason }), token); + } else { + return []; + } + })); + disposables.push(connection.onDidSaveTextDocument((event: DidSaveTextDocumentParams) => { + const syncedDocument = this._syncedDocuments.get(UriUtils.normalize(event.textDocument.uri)); + if (syncedDocument !== undefined) { + this._onDidSave.fire(Object.freeze({ document: syncedDocument })); + } + })); + return Disposable.create(() => { disposables.forEach(disposable => disposable.dispose()); }); + } +} diff --git a/packages/langium/src/utils/uri-utils.ts b/packages/langium/src/utils/uri-utils.ts index c7d0d5760..81fb80d43 100644 --- a/packages/langium/src/utils/uri-utils.ts +++ b/packages/langium/src/utils/uri-utils.ts @@ -36,4 +36,8 @@ export namespace UriUtils { return backPart + toPart; } + export function normalize(uri: URI | string): string { + return URI.parse(uri.toString()).toString(); + } + } diff --git a/packages/langium/test/utils/uri-utils.test.ts b/packages/langium/test/utils/uri-utils.test.ts index 004d6818e..b4686d238 100644 --- a/packages/langium/test/utils/uri-utils.test.ts +++ b/packages/langium/test/utils/uri-utils.test.ts @@ -52,3 +52,23 @@ describe('URI Utils', () => { expect(UriUtils.equals(uri1, undefined)).toBeFalsy(); }); }); + +describe('URIUtils#normalize', () => { + + test('Should normalize document URIs', () => { + const vscodeWindowsUri = 'file:///c%3A/path/to/file.txt'; + const upperCaseDriveUri = 'file:///C:/path/to/file.txt'; + const lowerCaseDriveUri = 'file:///c:/path/to/file.txt'; + + const normalized = vscodeWindowsUri; + expect(UriUtils.normalize(vscodeWindowsUri)).toBe(normalized); + expect(UriUtils.normalize(upperCaseDriveUri)).toBe(normalized); + expect(UriUtils.normalize(lowerCaseDriveUri)).toBe(normalized); + }); + + test('Should work as usual with POSIX URIs', () => { + const uri = 'file:///path/to/file.txt'; + expect(UriUtils.normalize(uri)).toBe(uri); + }); + +}); From db8aed15eda996c8dcaff1985258033fa9159394 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 4 Sep 2024 10:13:09 +0000 Subject: [PATCH 2/4] Add more documentation --- packages/langium/src/lsp/normalized-text-documents.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/langium/src/lsp/normalized-text-documents.ts b/packages/langium/src/lsp/normalized-text-documents.ts index 88465bc92..c0e071cb4 100644 --- a/packages/langium/src/lsp/normalized-text-documents.ts +++ b/packages/langium/src/lsp/normalized-text-documents.ts @@ -11,6 +11,11 @@ import type { import { TextDocumentSyncKind, Disposable, Emitter } from 'vscode-languageserver'; import { UriUtils } from '../utils/uri-utils.js'; +/** + * A manager service that keeps track of all currently opened text documents. + * + * Designed to be compatible with the `TextDocuments` class in the `vscode-languageserver` package. + */ export interface TextDocuments { /** * An event that fires when a text document managed by this manager From 47ae6b85a0350bf078b4d45e8e4aa9cb46e15eb9 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 4 Sep 2024 13:42:11 +0200 Subject: [PATCH 3/4] Extend text documents API --- examples/domainmodel/test/refs-index.test.ts | 8 +- .../src/lsp/normalized-text-documents.ts | 37 ++++++++- packages/langium/src/service-registry.ts | 2 +- packages/langium/src/test/langium-test.ts | 9 +- .../langium/src/workspace/document-builder.ts | 2 +- packages/langium/src/workspace/documents.ts | 2 +- packages/langium/test/utils/caching.test.ts | 5 +- .../test/workspace/document-builder.test.ts | 83 ++++++++----------- .../test/workspace/document-factory.test.ts | 3 +- 9 files changed, 85 insertions(+), 66 deletions(-) diff --git a/examples/domainmodel/test/refs-index.test.ts b/examples/domainmodel/test/refs-index.test.ts index 3c621bfc6..a129b924a 100644 --- a/examples/domainmodel/test/refs-index.test.ts +++ b/examples/domainmodel/test/refs-index.test.ts @@ -6,7 +6,7 @@ import type { AstNode, LangiumDocument, ReferenceDescription, URI } from 'langium'; import { AstUtils, EmptyFileSystem, TextDocument } from 'langium'; -import { parseDocument, setTextDocument } from 'langium/test'; +import { parseDocument } from 'langium/test'; import { describe, expect, test } from 'vitest'; import { createDomainModelServices } from '../src/language-server/domain-model-module.js'; import type { Domainmodel } from '../src/language-server/generated/ast.js'; @@ -20,15 +20,15 @@ describe('Cross references indexed after affected process', () => { let allRefs = await getReferences((superDoc.parseResult.value as Domainmodel).elements[0]); expect(allRefs.length).toEqual(0); // linking error - setTextDocument( - shared, + shared.workspace.TextDocuments.set( TextDocument.create( - superDoc.textDocument.uri.toString(), + superDoc.textDocument.uri, superDoc.textDocument.languageId, 0, 'entity SuperEntity {}' ) ); + await shared.workspace.DocumentBuilder.update([superDoc.uri], []); const updatedSuperDoc = await shared.workspace.LangiumDocuments.getOrCreateDocument(superDoc.uri); diff --git a/packages/langium/src/lsp/normalized-text-documents.ts b/packages/langium/src/lsp/normalized-text-documents.ts index c0e071cb4..81745f141 100644 --- a/packages/langium/src/lsp/normalized-text-documents.ts +++ b/packages/langium/src/lsp/normalized-text-documents.ts @@ -9,6 +9,7 @@ import type { TextDocumentWillSaveEvent, RequestHandler, TextEdit, Event, WillSaveTextDocumentParams, CancellationToken, DidSaveTextDocumentParams } from 'vscode-languageserver'; import { TextDocumentSyncKind, Disposable, Emitter } from 'vscode-languageserver'; +import type { URI } from '../utils/uri-utils.js'; import { UriUtils } from '../utils/uri-utils.js'; /** @@ -54,7 +55,17 @@ export interface TextDocuments { * @param uri The text document's URI to retrieve. * @return the text document or `undefined`. */ - get(uri: string): T | undefined; + get(uri: string | URI): T | undefined; + /** + * Sets the text document managed by this instance. + * @param document The text document to add. + * @returns `true` if the document didn't exist yet, `false` if it was already present. + */ + set(document: T): boolean; + /** + * Deletes a text document managed by this instance. + */ + delete(uri: string | URI | T): void; /** * Returns all text documents managed by this instance. * @@ -142,8 +153,28 @@ export class NormalizedTextDocuments implements TextD return this._onDidClose.event; } - public get(uri: string): T | undefined { - return this._syncedDocuments.get(UriUtils.normalize(uri)); + public get(uri: string | URI): T | undefined { + return this._syncedDocuments.get(UriUtils.normalize(uri.toString())); + } + + public set(document: T): boolean { + const uri = UriUtils.normalize(document.uri); + let result = true; + if (this._syncedDocuments.has(uri)) { + result = false; + } + this._syncedDocuments.set(uri, document); + this._onDidOpen.fire(Object.freeze({ document })); + return result; + } + + public delete(uri: string | T | URI): void { + const uriString = UriUtils.normalize(typeof uri === 'string' ? uri : 'uri' in uri ? uri.uri : uri.toString()); + const syncedDocument = this._syncedDocuments.get(uriString); + if (syncedDocument !== undefined) { + this._syncedDocuments.delete(uriString); + this._onDidClose.fire(Object.freeze({ document: syncedDocument })); + } } public all(): T[] { diff --git a/packages/langium/src/service-registry.ts b/packages/langium/src/service-registry.ts index 87ceef91e..bbf55bfd7 100644 --- a/packages/langium/src/service-registry.ts +++ b/packages/langium/src/service-registry.ts @@ -81,7 +81,7 @@ export class DefaultServiceRegistry implements ServiceRegistry { if (this.languageIdMap.size === 0) { throw new Error('The service registry is empty. Use `register` to register the services of a language.'); } - const languageId = this.textDocuments?.get(uri.toString())?.languageId; + const languageId = this.textDocuments?.get(uri)?.languageId; if (languageId !== undefined) { const services = this.languageIdMap.get(languageId); if (services) { diff --git a/packages/langium/src/test/langium-test.ts b/packages/langium/src/test/langium-test.ts index 73d703c0f..19ec41cc7 100644 --- a/packages/langium/src/test/langium-test.ts +++ b/packages/langium/src/test/langium-test.ts @@ -701,14 +701,15 @@ export function expectWarning { - textDocuments._syncedDocuments.delete(document.uri); + textDocuments.delete(document.uri); }); } diff --git a/packages/langium/src/workspace/document-builder.ts b/packages/langium/src/workspace/document-builder.ts index 0331febb9..efda59692 100644 --- a/packages/langium/src/workspace/document-builder.ts +++ b/packages/langium/src/workspace/document-builder.ts @@ -272,7 +272,7 @@ export class DefaultDocumentBuilder implements DocumentBuilder { } private hasTextDocument(doc: LangiumDocument): boolean { - return Boolean(this.textDocuments?.get(doc.uri.toString())); + return Boolean(this.textDocuments?.get(doc.uri)); } /** diff --git a/packages/langium/src/workspace/documents.ts b/packages/langium/src/workspace/documents.ts index 369a8d387..a94dc3563 100644 --- a/packages/langium/src/workspace/documents.ts +++ b/packages/langium/src/workspace/documents.ts @@ -112,7 +112,7 @@ export interface DocumentSegment { * No implementation object is expected to be offered by `LangiumCoreServices`, but only by `LangiumLSPServices`. */ export type TextDocumentProvider = { - get(uri: string): TextDocument | undefined + get(uri: string | URI): TextDocument | undefined } /** diff --git a/packages/langium/test/utils/caching.test.ts b/packages/langium/test/utils/caching.test.ts index a6f89e8ba..97ab192a0 100644 --- a/packages/langium/test/utils/caching.test.ts +++ b/packages/langium/test/utils/caching.test.ts @@ -10,14 +10,13 @@ import { beforeEach, describe, expect, test } from 'vitest'; import type { DefaultDocumentBuilder} from 'langium'; import { DocumentCache, EmptyFileSystem, URI, WorkspaceCache } from 'langium'; import { createLangiumGrammarServices } from 'langium/grammar'; -import { setTextDocument } from 'langium/test'; const services = createLangiumGrammarServices(EmptyFileSystem); const workspace = services.shared.workspace; const document1 = workspace.LangiumDocumentFactory.fromString('', URI.file('/document1.langium')); -setTextDocument(services.shared, document1.textDocument); +workspace.TextDocuments.set(document1.textDocument); const document2 = workspace.LangiumDocumentFactory.fromString('', URI.file('/document2.langium')); -setTextDocument(services.shared, document2.textDocument); +workspace.TextDocuments.set(document2.textDocument); workspace.LangiumDocuments.addDocument(document1); workspace.LangiumDocuments.addDocument(document2); diff --git a/packages/langium/test/workspace/document-builder.test.ts b/packages/langium/test/workspace/document-builder.test.ts index 27d5a6c0e..0fe779100 100644 --- a/packages/langium/test/workspace/document-builder.test.ts +++ b/packages/langium/test/workspace/document-builder.test.ts @@ -4,14 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { AstNode, DocumentBuilder, FileSystemProvider, LangiumDocument, LangiumDocumentFactory, LangiumDocuments, Module, Reference, TextDocumentProvider, ValidationChecks } from 'langium'; +import type { AstNode, DocumentBuilder, FileSystemProvider, LangiumDocument, LangiumDocumentFactory, LangiumDocuments, Module, Reference, ValidationChecks } from 'langium'; import { AstUtils, DocumentState, TextDocument, URI, isOperationCancelled } from 'langium'; import { createServicesForGrammar } from 'langium/grammar'; -import { setTextDocument } from 'langium/test'; import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; import { CancellationToken, CancellationTokenSource } from 'vscode-languageserver'; import { fail } from 'assert'; -import type { LangiumServices, LangiumSharedServices } from '../../lib/lsp/lsp-services.js'; +import type { LangiumServices, LangiumSharedServices, TextDocuments } from 'langium/lsp'; describe('DefaultDocumentBuilder', () => { async function createServices(shared?: Module) { @@ -52,32 +51,34 @@ describe('DefaultDocumentBuilder', () => { test('emits `onUpdate` on `update` call', async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document = documentFactory.fromString('', URI.parse('file:///test1.txt')); documents.addDocument(document); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; await builder.build([document], {}); - setTextDocument(services, document.textDocument); let called = false; builder.onUpdate(() => { called = true; }); + workspace.TextDocuments.set(document.textDocument); await builder.update([document.uri], []); expect(called).toBe(true); }); test('emits `onUpdate` on `build` call', async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document = documentFactory.fromString('', URI.parse('file:///test1.txt')); documents.addDocument(document); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; await builder.build([document], {}); - setTextDocument(services, document.textDocument); + workspace.TextDocuments.set(document.textDocument); let called = false; builder.onUpdate(() => { called = true; @@ -127,8 +128,9 @@ describe('DefaultDocumentBuilder', () => { test('resumes document build after cancellation', async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document1 = documentFactory.fromString(` foo 1 A foo 11 B @@ -144,7 +146,7 @@ describe('DefaultDocumentBuilder', () => { `, URI.parse('file:///test2.txt')); documents.addDocument(document2); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; const tokenSource1 = new CancellationTokenSource(); builder.onBuildPhase(DocumentState.IndexedContent, () => { tokenSource1.cancel(); @@ -157,7 +159,7 @@ describe('DefaultDocumentBuilder', () => { expect(document1.state).toBe(DocumentState.IndexedContent); expect(document2.state).toBe(DocumentState.IndexedContent); - setTextDocument(services, document1.textDocument); + workspace.TextDocuments.set(document1.textDocument); await builder.update([document1.uri], []); // While the first document is built with validation due to its reported update, the second one // is resumed with its initial build options, which did not include validation. @@ -171,8 +173,9 @@ describe('DefaultDocumentBuilder', () => { test('includes document with references to updated document', async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document1 = documentFactory.fromString(` foo 1 A foo 11 B @@ -187,20 +190,20 @@ describe('DefaultDocumentBuilder', () => { `, URI.parse('file:///test2.txt')); documents.addDocument(document2); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; await builder.build([document1, document2], {}); expect(document1.state).toBe(DocumentState.IndexedReferences); expect(document1.references.filter(r => r.error !== undefined)).toHaveLength(0); expect(document2.state).toBe(DocumentState.IndexedReferences); expect(document2.references.filter(r => r.error !== undefined)).toHaveLength(0); - setTextDocument(services, document1.textDocument); + workspace.TextDocuments.set(document1.textDocument); TextDocument.update(document1.textDocument, [{ // Change `foo 1 A` to `foo 1 D`, breaking the local reference range: { start: { line: 1, character: 18 }, end: { line: 1, character: 19 } }, text: 'D' }], 1); - setTextDocument(services, document2.textDocument); + workspace.TextDocuments.set(document2.textDocument); builder.updateBuildOptions = { validation: { // Only the linking error is reported for the first document @@ -442,8 +445,9 @@ describe('DefaultDocumentBuilder', () => { test("References are unlinked on update even though the document didn't change", async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document = documentFactory.fromString(` foo 1 A foo 11 B @@ -452,12 +456,12 @@ describe('DefaultDocumentBuilder', () => { `, URI.parse('file:///test1.txt')); documents.addDocument(document); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; await builder.build([document], { validation: true }); expect(document.state).toBe(DocumentState.Validated); expect(document.references).toHaveLength(2); - setTextDocument(services, document.textDocument); + workspace.TextDocuments.set(document.textDocument); try { // Immediately cancel the update to prevent the document from being rebuilt await builder.update([document.uri], [], CancellationToken.Cancelled); @@ -478,8 +482,9 @@ describe('DefaultDocumentBuilder', () => { test("References are unlinked on update even if the document didn't reach linked phase yet", async () => { const services = await createServices(); - const documentFactory = services.shared.workspace.LangiumDocumentFactory; - const documents = services.shared.workspace.LangiumDocuments; + const workspace = services.shared.workspace; + const documentFactory = workspace.LangiumDocumentFactory; + const documents = workspace.LangiumDocuments; const document = documentFactory.fromString(` foo 1 A foo 11 B @@ -489,7 +494,7 @@ describe('DefaultDocumentBuilder', () => { documents.addDocument(document); const tokenSource = new CancellationTokenSource(); - const builder = services.shared.workspace.DocumentBuilder; + const builder = workspace.DocumentBuilder; builder.onBuildPhase(DocumentState.ComputedScopes, () => { tokenSource.cancel(); }); @@ -511,7 +516,7 @@ describe('DefaultDocumentBuilder', () => { expect(document.references).toHaveLength(1); - setTextDocument(services, document.textDocument); + workspace.TextDocuments.set(document.textDocument); try { // Immediately cancel the update to prevent the document from being rebuilt await builder.update([document.uri], [], CancellationToken.Cancelled); @@ -535,7 +540,7 @@ describe('DefaultDocumentBuilder', () => { let documentFactory: LangiumDocumentFactory; let documents: LangiumDocuments; let builder: DocumentBuilder; - let textDocuments: TextDocumentProvider; + let textDocuments: TextDocuments; let sortSpy: ReturnType; beforeEach(async () => { @@ -564,9 +569,7 @@ describe('DefaultDocumentBuilder', () => { } async function openDocuments(docs: LangiumDocument[]): Promise { - docs.forEach(doc => { - (textDocuments as unknown as MockTextDocumentProvider).addDocument(doc.uri.toString(), doc.textDocument); - }); + docs.forEach(doc => textDocuments.set(doc.textDocument)); } async function updateAndGetSortedDocuments(docs: LangiumDocument[]): Promise { @@ -576,7 +579,7 @@ describe('DefaultDocumentBuilder', () => { } function isDocumentOpen(doc: LangiumDocument): boolean { - return Boolean((textDocuments as unknown as MockTextDocumentProvider).get(doc.uri.toString())); + return Boolean(textDocuments.get(doc.uri)); } test('Open documents are sorted before closed documents', async () => { @@ -643,19 +646,6 @@ describe('DefaultDocumentBuilder', () => { }); }); -class MockTextDocumentProvider implements TextDocumentProvider { - isMockTextDocumentProvider = true; - private docs: Map = new Map(); - - // simulate opening a file in the mock text document provider - addDocument(uri: string, document: TextDocument): void { - this.docs.set(uri, document); - } - - get(uri: string): TextDocument | undefined { - return this.docs.get(uri); - } -} class MockFileSystemProvider implements FileSystemProvider { isMockFileSystemProvider = true; @@ -672,7 +662,6 @@ class MockFileSystemProvider implements FileSystemProvider { export const mockSharedModule: Module = { workspace: { - TextDocuments: () => new MockTextDocumentProvider(), FileSystemProvider: () => new MockFileSystemProvider() } }; diff --git a/packages/langium/test/workspace/document-factory.test.ts b/packages/langium/test/workspace/document-factory.test.ts index 7e8c5c126..0dc835a42 100644 --- a/packages/langium/test/workspace/document-factory.test.ts +++ b/packages/langium/test/workspace/document-factory.test.ts @@ -9,7 +9,6 @@ import type { LangiumServices } from 'langium/lsp'; import { describe, expect, test } from 'vitest'; import { DocumentState, EmptyFileSystem, TextDocument } from 'langium'; import { createLangiumGrammarServices } from 'langium/grammar'; -import { setTextDocument } from 'langium/test'; import { CancellationToken } from 'vscode-languageserver'; describe('DefaultLangiumDocumentFactory', () => { @@ -50,6 +49,6 @@ describe('DefaultLangiumDocumentFactory', () => { function createTextDocument(uri: string, text: string, services: LangiumServices): TextDocument { const document = TextDocument.create(uri, 'langium', 0, text); - setTextDocument(services, document); + services.shared.workspace.TextDocuments.set(document); return document; } From 29d2eeeb8cf6ebcc4d5fb5e92d5007c032ce6fcd Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 4 Sep 2024 14:59:54 +0200 Subject: [PATCH 4/4] Review comment --- packages/langium/src/lsp/normalized-text-documents.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/langium/src/lsp/normalized-text-documents.ts b/packages/langium/src/lsp/normalized-text-documents.ts index 81745f141..edf886585 100644 --- a/packages/langium/src/lsp/normalized-text-documents.ts +++ b/packages/langium/src/lsp/normalized-text-documents.ts @@ -154,7 +154,7 @@ export class NormalizedTextDocuments implements TextD } public get(uri: string | URI): T | undefined { - return this._syncedDocuments.get(UriUtils.normalize(uri.toString())); + return this._syncedDocuments.get(UriUtils.normalize(uri)); } public set(document: T): boolean { @@ -164,12 +164,14 @@ export class NormalizedTextDocuments implements TextD result = false; } this._syncedDocuments.set(uri, document); - this._onDidOpen.fire(Object.freeze({ document })); + const toFire = Object.freeze({ document }); + this._onDidOpen.fire(toFire); + this._onDidChangeContent.fire(toFire); return result; } public delete(uri: string | T | URI): void { - const uriString = UriUtils.normalize(typeof uri === 'string' ? uri : 'uri' in uri ? uri.uri : uri.toString()); + const uriString = UriUtils.normalize(typeof uri === 'object' && 'uri' in uri ? uri.uri : uri); const syncedDocument = this._syncedDocuments.get(uriString); if (syncedDocument !== undefined) { this._syncedDocuments.delete(uriString);