Skip to content

Commit

Permalink
adjustments per review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
montymxb committed Dec 13, 2023
1 parent 54608fc commit 2ea63ab
Show file tree
Hide file tree
Showing 17 changed files with 148 additions and 260 deletions.
2 changes: 1 addition & 1 deletion examples/domainmodel/src/cli/cli-util.ts
Expand Up @@ -52,7 +52,7 @@ export async function setRootFolder(fileName: string, services: LangiumServices,
}
const folders: WorkspaceFolder[] = [{
name: path.basename(root),
uri: URI.file(root)
uri: URI.file(root).toString()
}];
await services.shared.workspace.WorkspaceManager.initializeWorkspace(folders);
}
Expand Down
3 changes: 1 addition & 2 deletions examples/domainmodel/test/domainmodel-cli.test.ts
Expand Up @@ -37,8 +37,7 @@ describe('Test the domainmodel CLI', () => {
}

test('Test action without CLI', async () => {
// TODO @montymxb, revert to quiet: true when the import issue is fixed
await generateAction(rawfileName, { destination, quiet: false });
await generateAction(rawfileName, { destination, quiet: true });
commonExpectations();
});

Expand Down
2 changes: 1 addition & 1 deletion examples/requirements/src/cli/cli-util.ts
Expand Up @@ -33,7 +33,7 @@ export async function extractDocuments(fileName: string, services: LangiumCoreSe
}

const folders: WorkspaceFolder[] = [{
uri: URI.file(path.resolve(path.dirname(fileName))),
uri: URI.file(path.resolve(path.dirname(fileName))).toString(),
name: 'main'
}];
await services.shared.workspace.WorkspaceManager.initializeWorkspace(folders);
Expand Down
3 changes: 1 addition & 2 deletions packages/generator-langium/templates/cli/src/cli/cli-util.ts
@@ -1,5 +1,4 @@
import type { AstNode, LangiumCoreServices, LangiumDocument } from 'langium';
import type { LangiumServices } from 'langium/lsp';
import chalk from 'chalk';
import * as path from 'node:path';
import * as fs from 'node:fs';
Expand Down Expand Up @@ -34,7 +33,7 @@ export async function extractDocument(fileName: string, services: LangiumCoreSer
return document;
}

export async function extractAstNode<T extends AstNode>(fileName: string, services: LangiumServices): Promise<T> {
export async function extractAstNode<T extends AstNode>(fileName: string, services: LangiumCoreServices): Promise<T> {
return (await extractDocument(fileName, services)).parseResult?.value as T;
}

Expand Down
Expand Up @@ -7,7 +7,7 @@
import type { Ignore } from 'ignore';
import type { LangiumSharedCoreServices, ConfigurationProvider, FileSystemNode, WorkspaceFolder } from 'langium';
import ignore from 'ignore';
import { DefaultWorkspaceManager, UriUtils } from 'langium';
import { DefaultWorkspaceManager, URI, UriUtils } from 'langium';
import * as path from 'path';
import { CancellationToken } from 'vscode-languageserver-protocol';

Expand Down Expand Up @@ -40,7 +40,7 @@ export class LangiumGrammarWorkspaceManager extends DefaultWorkspaceManager {
protected override includeEntry(workspaceFolder: WorkspaceFolder, entry: FileSystemNode, fileExtensions: string[]): boolean {
if (this.matcher) {
// create path relative to workspace folder root: /user/foo/workspace/entry.txt -> entry.txt
const relPath = path.relative(workspaceFolder.uri.path, entry.uri.path);
const relPath = path.relative(URI.parse(workspaceFolder.uri).path, entry.uri.path);
const ignored = this.matcher.ignores(relPath);
return !ignored && (entry.isDirectory || (entry.isFile && fileExtensions.includes(UriUtils.extname(entry.uri))));
}
Expand Down
4 changes: 2 additions & 2 deletions packages/langium/src/default-module.ts
Expand Up @@ -33,7 +33,7 @@ import { JSDocDocumentationProvider } from './documentation/documentation-provid
import { DefaultCommentProvider } from './documentation/comment-provider.js';
import { LangiumParserErrorMessageProvider } from './parser/langium-parser.js';
import { DefaultWorkspaceLock } from './workspace/workspace-lock.js';
import { EmptyConfigurationProvider } from './index.js';
import { InMemoryConfigurationProvider } from './workspace/configuration.js';

/**
* Context required for creating the default language-specific dependency injection module.
Expand Down Expand Up @@ -117,7 +117,7 @@ export function createDefaultSharedModule(context: DefaultSharedModuleContext):
WorkspaceManager: (services) => new DefaultWorkspaceManager(services),
FileSystemProvider: (services) => context.fileSystemProvider(services),
WorkspaceLock: () => new DefaultWorkspaceLock(),
ConfigurationProvider: (services) => new EmptyConfigurationProvider(services)
ConfigurationProvider: (services) => new InMemoryConfigurationProvider(services)
}
};
}
6 changes: 3 additions & 3 deletions packages/langium/src/lsp/langium-lsp-module.ts
Expand Up @@ -27,9 +27,9 @@ import { LangiumGrammarGeneratedModule, LangiumGrammarGeneratedSharedModule } fr
import { registerValidationChecks } from '../grammar/validation/validator.js';
import { registerTypeValidationChecks } from '../grammar/validation/types-validator.js';
import { DefaultDocumentUpdateHandler } from './document-update-handler.js';
import { LSPConfigurationProvider } from './workspace/default-configuration.js';
import { LSPLangiumDocumentFactory } from './workspace/documents.js';
import { LSPWorkspaceManager } from './workspace/workspace-manager.js';
import { LSPConfigurationProvider } from './workspace/lsp-configuration.js';
import { LSPLangiumDocumentFactory } from './workspace/lsp-documents.js';
import { LSPWorkspaceManager } from './workspace/lsp-workspace-manager.js';
import { CoreServiceRegistry } from '../service-registry.js';

/**
Expand Down
46 changes: 6 additions & 40 deletions packages/langium/src/lsp/workspace/lsp-configuration.ts
Expand Up @@ -4,28 +4,21 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type { Connection, DidChangeConfigurationParams } from 'vscode-languageserver';
import type { Connection } from 'vscode-languageserver';
import type { ConfigurationItem } from 'vscode-languageserver-protocol';
import type { ICoreServiceRegistry } from '../../service-registry.js';
import { DidChangeConfigurationNotification } from 'vscode-languageserver-protocol';
import type { ConfigurationProvider } from '../../workspace/configuration.js';
import { AbstractConfigurationProvider } from '../../workspace/configuration.js';
import type { LangiumSharedServices } from '../lsp-services.js';

/* eslint-disable @typescript-eslint/no-explicit-any */

/**
* Configuration provider that uses LSP services to function
* A configuration provider that uses LSP services to function
*/
export class LSPConfigurationProvider implements ConfigurationProvider {
export class LSPConfigurationProvider extends AbstractConfigurationProvider {

protected settings: Record<string, Record<string, any>> = {};
protected workspaceConfig = false;
protected initialized = false;
protected readonly serviceRegistry: ICoreServiceRegistry;
protected readonly connection: Connection | undefined;

constructor(services: LangiumSharedServices) {
this.serviceRegistry = services.ServiceRegistry;
super(services);
this.connection = services.lsp.Connection;
services.lsp.LanguageServer.onInitialize(params => {
this.workspaceConfig = params.capabilities.workspace?.configuration ?? false;
Expand All @@ -39,7 +32,7 @@ export class LSPConfigurationProvider implements ConfigurationProvider {
});
}

protected async initialize(): Promise<void> {
protected override async initialize(): Promise<void> {
if (this.workspaceConfig && this.connection) {
const languages = this.serviceRegistry.all;
const configToUpdate: ConfigurationItem[] = languages.map(lang => { return { section: this.toSectionName(lang.LanguageMetaData.languageId) }; });
Expand All @@ -51,31 +44,4 @@ export class LSPConfigurationProvider implements ConfigurationProvider {
}
this.initialized = true;
}

updateConfiguration(change: DidChangeConfigurationParams): void {
if (!change.settings) {
return;
}
Object.keys(change.settings).forEach(section => {
this.updateSectionConfiguration(section, change.settings[section]);
});
}

protected updateSectionConfiguration(section: string, configuration: any): void {
this.settings[section] = configuration;
}

async getConfiguration(language: string, configuration: string): Promise<any> {
if (!this.initialized) {
await this.initialize();
}
const sectionName = this.toSectionName(language);
if (this.settings[sectionName]) {
return this.settings[sectionName][configuration];
}
}

protected toSectionName(languageId: string): string {
return `${languageId}`;
}
}
139 changes: 25 additions & 114 deletions packages/langium/src/lsp/workspace/lsp-documents.ts
Expand Up @@ -5,149 +5,60 @@
******************************************************************************/

import type { TextDocuments } from 'vscode-languageserver';
import type { ParseResult } from '../../parser/langium-parser.js';
import type { ICoreServiceRegistry } from '../../service-registry.js';
import type { AstNode } from '../../syntax-tree.js';
import type { Mutable } from '../../utils/ast-util.js';
import type { FileSystemProvider } from './../../workspace/file-system-provider.js';
import { DocumentState, type LangiumDocument, type LangiumDocumentFactory } from '../../workspace/documents.js';
import { URI } from '../../utils/uri-util.js';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { DefaultLangiumDocumentFactory, DocumentState, type LangiumDocument } from '../../workspace/documents.js';
import type { URI } from '../../utils/uri-util.js';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { LangiumSharedServices } from '../lsp-services.js';

/**
* Document Factory that utilizes lsp TextDocuments (or the file system) to create/update LangiumDocuments
*/
export class LSPLangiumDocumentFactory implements LangiumDocumentFactory {
export class LSPLangiumDocumentFactory extends DefaultLangiumDocumentFactory {

protected readonly serviceRegistry: ICoreServiceRegistry;
protected readonly textDocuments: TextDocuments<TextDocument>;
protected readonly fileSystemProvider: FileSystemProvider;

constructor(services: LangiumSharedServices) {
this.serviceRegistry = services.ServiceRegistry;
super(services);
this.textDocuments = services.workspace.TextDocuments;
this.fileSystemProvider = services.workspace.FileSystemProvider;
}

fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI): LangiumDocument<T> {
return this.create<T>(uri ?? URI.parse(textDocument.uri), textDocument);
}

fromString<T extends AstNode = AstNode>(text: string, uri: URI): LangiumDocument<T> {
return this.create<T>(uri, text);
}

fromModel<T extends AstNode = AstNode>(model: T, uri: URI): LangiumDocument<T> {
return this.create<T>(uri, { $model: model });
}

create<T extends AstNode = AstNode>(uri: URI, content?: string | TextDocument | { $model: T }): LangiumDocument<T> {
override create<T extends AstNode = AstNode>(uri: URI, content?: string | TextDocument | { $model: T }): LangiumDocument<T> {
// if no document is given, check the textDocuments service first, it maintains documents being opened in an editor
content ??= this.textDocuments.get(uri.toString());
// if still no document is found try to load it from the file system
content ??= this.getContentFromFileSystem(uri);

if (typeof content === 'string') {
const parseResult = this.parse<T>(uri, content);
return this.createLangiumDocument<T>(parseResult, uri, undefined, content);

} else if ('$model' in content) {
const parseResult = { value: content.$model, parserErrors: [], lexerErrors: [] };
return this.createLangiumDocument<T>(parseResult, uri);

} else {
const parseResult = this.parse<T>(uri, content.getText());
return this.createLangiumDocument(parseResult, uri, content);
}
return super.create(uri, content);
}

/**
* Create a LangiumDocument from a given parse result.
*
* A TextDocument is created on demand if it is not provided as argument here. Usually this
* should not be necessary because the main purpose of the TextDocument is to convert between
* text ranges and offsets, which is done solely in LSP request handling.
*
* With the introduction of {@link update} below this method is supposed to be mainly called
* during workspace initialization and on addition/recognition of new files, while changes in
* existing documents are processed via {@link update}.
*/
protected createLangiumDocument<T extends AstNode = AstNode>(parseResult: ParseResult<T>, uri: URI, textDocument?: TextDocument, text?: string): LangiumDocument<T> {
let document: LangiumDocument<T>;
if (textDocument) {
document = {
parseResult,
uri,
state: DocumentState.Parsed,
references: [],
textDocument
};
} else {
const textDocumentGetter = this.createTextDocumentGetter(uri, text);
document = {
parseResult,
uri,
state: DocumentState.Parsed,
references: [],
get textDocument() {
return textDocumentGetter();
}
};
}
(parseResult.value as Mutable<AstNode>).$document = document;
return document;
}
override update<T extends AstNode = AstNode>(document: Mutable<LangiumDocument<T>>): LangiumDocument<T> {

update<T extends AstNode = AstNode>(document: Mutable<LangiumDocument<T>>): LangiumDocument<T> {
// The CST full text property contains the original text that was used to create the AST.
const oldText = document.parseResult.value.$cstNode?.root.fullText;
const textDocument = this.textDocuments.get(document.uri.toString());
const text = textDocument ? textDocument.getText() : this.getContentFromFileSystem(document.uri);

if (textDocument) {
// The CST full text property contains the original text that was used to create the AST.
const oldText = document.parseResult.value.$cstNode?.root.fullText;

// use the textDocument to update the document
Object.defineProperty(
document, 'textDocument',
{
value: textDocument
}
);
} else {
const textDocumentGetter = this.createTextDocumentGetter(document.uri, text);
Object.defineProperty(
document, 'textDocument',
{
get: textDocumentGetter
}
);
}

// Some of these documents can be pretty large, so parsing them again can be quite expensive.
// Therefore, we only parse if the text has actually changed.
if (oldText !== text) {
document.parseResult = this.parse(document.uri, text);
(document.parseResult.value as Mutable<AstNode>).$document = document;
}
document.state = DocumentState.Parsed;
return document;
}

protected getContentFromFileSystem(uri: URI): string {
return this.fileSystemProvider.readFileSync(uri);
}
const text = textDocument.getText();

protected parse<T extends AstNode>(uri: URI, text: string): ParseResult<T> {
const services = this.serviceRegistry.getServices(uri);
return services.parser.LangiumParser.parse<T>(text);
}
// Some of these documents can be pretty large, so parsing them again can be quite expensive.
// Therefore, we only parse if the text has actually changed.
if (oldText !== text) {
document.parseResult = this.parse(document.uri, text);
(document.parseResult.value as Mutable<AstNode>).$document = document;
}
document.state = DocumentState.Parsed;
return document;

protected createTextDocumentGetter(uri: URI, text?: string): () => TextDocument {
const serviceRegistry = this.serviceRegistry;
let textDoc: TextDocument | undefined = undefined;
return () => {
return textDoc ??= TextDocument.create(
uri.toString(), serviceRegistry.getServices(uri).LanguageMetaData.languageId, 0, text ?? ''
);
};
} else {
// otherwise use the default implementation
return super.update(document);
}
}
}
4 changes: 2 additions & 2 deletions packages/langium/src/lsp/workspace/lsp-workspace-manager.ts
Expand Up @@ -17,8 +17,8 @@ export class LSPWorkspaceManager extends DefaultWorkspaceManager {
constructor(services: LangiumSharedServices) {
super(services);
services.lsp.LanguageServer.onInitialize(params => {
// convert lsp workspace folders to langium workspace folders (equivalent type plays nice with the existing non-lsp core services)
this.folders = (params.workspaceFolders as unknown as WorkspaceFolder[]) ?? undefined;
// convert lsp workspace folders to langium workspace folders
this.folders = params.workspaceFolders ?? undefined;
});

services.lsp.LanguageServer.onInitialized(_params => {
Expand Down

0 comments on commit 2ea63ab

Please sign in to comment.