Skip to content

Commit

Permalink
Extend text documents API
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew committed Sep 4, 2024
1 parent db8aed1 commit 47ae6b8
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 66 deletions.
8 changes: 4 additions & 4 deletions examples/domainmodel/test/refs-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
37 changes: 34 additions & 3 deletions packages/langium/src/lsp/normalized-text-documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -54,7 +55,17 @@ export interface TextDocuments<T extends { uri: string }> {
* @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.
*
Expand Down Expand Up @@ -142,8 +153,28 @@ export class NormalizedTextDocuments<T extends { uri: string }> 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[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/langium/src/service-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 5 additions & 4 deletions packages/langium/src/test/langium-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,14 +701,15 @@ export function expectWarning<T extends AstNode = AstNode, N extends AstNode = A

/**
* Add the given document to the `TextDocuments` service, simulating it being opened in an editor.
*
* @deprecated Since 3.2.0. Use `set`/`delete` from `TextDocuments` instead.
*/
export function setTextDocument(services: LangiumServices | LangiumSharedLSPServices, document: TextDocument): Disposable {
const shared = 'shared' in services ? services.shared : services;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const textDocuments = shared.workspace.TextDocuments as any;
textDocuments._syncedDocuments.set(document.uri, document);
const textDocuments = shared.workspace.TextDocuments;
textDocuments.set(document);
return Disposable.create(() => {
textDocuments._syncedDocuments.delete(document.uri);
textDocuments.delete(document.uri);
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/langium/src/workspace/document-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/langium/src/workspace/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
5 changes: 2 additions & 3 deletions packages/langium/test/utils/caching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
83 changes: 36 additions & 47 deletions packages/langium/test/workspace/document-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LangiumSharedServices, object>) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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();
});
Expand All @@ -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);
Expand All @@ -535,7 +540,7 @@ describe('DefaultDocumentBuilder', () => {
let documentFactory: LangiumDocumentFactory;
let documents: LangiumDocuments;
let builder: DocumentBuilder;
let textDocuments: TextDocumentProvider;
let textDocuments: TextDocuments<TextDocument>;
let sortSpy: ReturnType<typeof vi.spyOn>;

beforeEach(async () => {
Expand Down Expand Up @@ -564,9 +569,7 @@ describe('DefaultDocumentBuilder', () => {
}

async function openDocuments(docs: LangiumDocument[]): Promise<void> {
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<LangiumDocument[]> {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -643,19 +646,6 @@ describe('DefaultDocumentBuilder', () => {
});
});

class MockTextDocumentProvider implements TextDocumentProvider {
isMockTextDocumentProvider = true;
private docs: Map<string, TextDocument> = 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;

Expand All @@ -672,7 +662,6 @@ class MockFileSystemProvider implements FileSystemProvider {

export const mockSharedModule: Module<LangiumSharedServices, object> = {
workspace: {
TextDocuments: () => new MockTextDocumentProvider(),
FileSystemProvider: () => new MockFileSystemProvider()
}
};
Expand Down
Loading

0 comments on commit 47ae6b8

Please sign in to comment.