Skip to content

Commit

Permalink
splits up config, docs, and workspace services into lsp/non-lsp versions
Browse files Browse the repository at this point in the history
  • Loading branch information
montymxb committed Nov 17, 2023
1 parent b98b5c8 commit 643ff90
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 101 deletions.
3 changes: 2 additions & 1 deletion examples/domainmodel/src/cli/cli-util.ts
Expand Up @@ -10,6 +10,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import chalk from 'chalk';
import { URI } from 'langium';
import type { LangiumLSPServices } from 'langium/lsp';

export async function extractDocument<T extends AstNode>(fileName: string, extensions: readonly string[], services: LangiumServices): Promise<LangiumDocument<T>> {
if (!extensions.includes(path.extname(fileName))) {
Expand Down Expand Up @@ -43,7 +44,7 @@ export async function extractAstNode<T extends AstNode>(fileName: string, extens
return (await extractDocument(fileName, extensions, services)).parseResult.value as T;
}

export async function setRootFolder(fileName: string, services: LangiumServices, root?: string): Promise<void> {
export async function setRootFolder(fileName: string, services: LangiumServices & LangiumLSPServices, root?: string): Promise<void> {
if (!root) {
root = path.dirname(fileName);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/langium-sprotty/src/sprotty-services.ts
Expand Up @@ -5,6 +5,7 @@
******************************************************************************/

import type { LangiumServices, LangiumSharedServices } from 'langium';
import type { LangiumSharedLSPServices } from 'langium/lsp';
import type { DiagramOptions, DiagramServer, DiagramServices } from 'sprotty-protocol';
import type { DiagramServerManager } from './diagram-server-manager.js';

Expand Down Expand Up @@ -33,4 +34,4 @@ export type SprottySharedServices = {
/**
* Extension of the `LangiumSharedServices` with the diagram-related shared services.
*/
export type LangiumSprottySharedServices = LangiumSharedServices & SprottySharedServices
export type LangiumSprottySharedServices = LangiumSharedServices & LangiumSharedLSPServices & SprottySharedServices
5 changes: 2 additions & 3 deletions packages/langium/src/default-module.ts
Expand Up @@ -25,7 +25,6 @@ import { DefaultDocumentValidator } from './validation/document-validator.js';
import { ValidationRegistry } from './validation/validation-registry.js';
import { DefaultAstNodeDescriptionProvider, DefaultReferenceDescriptionProvider } from './workspace/ast-descriptions.js';
import { DefaultAstNodeLocator } from './workspace/ast-node-locator.js';
import { DefaultConfigurationProvider } from './workspace/configuration.js';
import { DefaultDocumentBuilder } from './workspace/document-builder.js';
import { DefaultLangiumDocumentFactory, DefaultLangiumDocuments } from './workspace/documents.js';
import { DefaultIndexManager } from './workspace/index-manager.js';
Expand All @@ -34,6 +33,7 @@ import { DefaultLexer } from './parser/lexer.js';
import { JSDocDocumentationProvider } from './documentation/documentation-provider.js';
import { DefaultCommentProvider } from './documentation/comment-provider.js';
import { LangiumParserErrorMessageProvider } from './parser/langium-parser.js';
import { EmptyConfigurationProvider } from './index.js';

/**
* Context required for creating the default language-specific dependency injection module.
Expand Down Expand Up @@ -113,12 +113,11 @@ export function createDefaultSharedModule(context: DefaultSharedModuleContext):
LangiumDocuments: (services) => new DefaultLangiumDocuments(services),
LangiumDocumentFactory: (services) => new DefaultLangiumDocumentFactory(services),
DocumentBuilder: (services) => new DefaultDocumentBuilder(services),
// TODO @montymxb: Factored out TextDocuments from here since it's tied to the LSP, unsure if this is the right choice
IndexManager: (services) => new DefaultIndexManager(services),
WorkspaceManager: (services) => new DefaultWorkspaceManager(services),
FileSystemProvider: (services) => context.fileSystemProvider(services),
MutexLock: () => new MutexLock(),
ConfigurationProvider: (services) => new DefaultConfigurationProvider(services)
ConfigurationProvider: (services) => new EmptyConfigurationProvider(services)
}
};
}
3 changes: 2 additions & 1 deletion packages/langium/src/grammar/langium-grammar-module.ts
Expand Up @@ -28,7 +28,7 @@ import { LangiumGrammarTypesValidator, registerTypeValidationChecks } from './va
import { interruptAndCheck } from '../utils/promise-util.js';
import { DocumentState } from '../workspace/documents.js';
import type { LSPServices, LangiumLSPServices, LangiumSharedLSPServices } from '../lsp/lsp-services.js';
import { createLSPModule } from '../lsp/langium-lsp-module.js';
import { createLSPModule, createSharedLSPModule } from '../lsp/langium-lsp-module.js';

export type LangiumGrammarAddedServices = {
validation: {
Expand Down Expand Up @@ -72,6 +72,7 @@ export function createLangiumGrammarServices(context: DefaultSharedModuleContext
const shared = inject(
createDefaultSharedModule(context),
LangiumGrammarGeneratedSharedModule,
createSharedLSPModule(context),
sharedModule
);
const grammar = inject(
Expand Down
23 changes: 19 additions & 4 deletions packages/langium/src/lsp/langium-lsp-module.ts
Expand Up @@ -26,6 +26,9 @@ import { LangiumGrammarModule, type LangiumGrammarServices } from '../grammar/la
import { LangiumGrammarGeneratedModule, LangiumGrammarGeneratedSharedModule } from '../grammar/generated/module.js';
import { registerValidationChecks } from '../grammar/internal-grammar-util.js';
import { registerTypeValidationChecks } from '../grammar/validation/types-validator.js';
import { LSPConfigurationProvider } from './workspace/default-configuration.js';
import { LSPLangiumDocumentFactory } from './workspace/documents.js';
import { LSPWorkspaceManager } from './workspace/workspace-manager.js';

/**
* Context required for creating the default language-specific dependency injection module.
Expand Down Expand Up @@ -54,7 +57,10 @@ export function createLSPModule(context: LSPModuleContext): Module<LangiumServic
};
}

// TODO @montymxb the second part of this module still needs to be adjusted
/**
* Create a dependency injection module for the LSP shared services. This is the set of
* services that are shared between multiple languages.
*/
export function createSharedLSPModule(context: DefaultSharedModuleContext): Module<LangiumSharedLSPServices & LangiumSharedServices, LangiumSharedLSPServices> {
return {
lsp: {
Expand All @@ -65,13 +71,22 @@ export function createSharedLSPModule(context: DefaultSharedModuleContext): Modu
FuzzyMatcher: () => new DefaultFuzzyMatcher()
},
workspace: {
TextDocuments: () => new TextDocuments(TextDocument)
TextDocuments: () => new TextDocuments(TextDocument),
WorkspaceManager: (services) => new LSPWorkspaceManager(services),
ConfigurationProvider: (services) => new LSPConfigurationProvider(services),
LangiumDocumentFactory: (services) => new LSPLangiumDocumentFactory(services),
}
};
}

// TODO @montymxb still needs function header
// Mirrors 'createLangiumGrammarServices', needs to be refactored/renamed before this PR is done
// TODO Mirrors 'createLangiumGrammarServices', needs to be refactored/renamed before this PR is done
/**
* Creates Langium grammars services, enriched with LSP functionality
*
* @param context Shared module context, used to create additional shared modules
* @param sharedModule Existing shared module to inject together with new shared services
* @returns Shared services enriched with LSP services + Grammar services, per usual
*/
export function createLangiumGrammarServicesWithLSP(context: DefaultSharedModuleContext,
sharedModule?: Module<LangiumSharedServices & LangiumSharedLSPServices, PartialLangiumSharedServices & PartialLangiumSharedLSPServices>): {
shared: LangiumSharedServices & LangiumSharedLSPServices,
Expand Down
7 changes: 3 additions & 4 deletions packages/langium/src/lsp/language-server.ts
Expand Up @@ -85,14 +85,13 @@ export class DefaultLanguageServer implements LanguageServer {
}

protected hasService(callback: (language: LangiumCombinedServices) => object | undefined): boolean {
protected hasService(callback: (language: LangiumServices) => object | undefined): boolean {
return this.services.ServiceRegistry.all.some(language => callback(language) !== undefined);
// TODO @montymxb downcast is not safe, needs additional work
return this.services.ServiceRegistry.all.some(language => callback(language as LangiumCombinedServices) !== undefined);
}

protected buildInitializeResult(_params: InitializeParams): InitializeResult {
const languages = this.services.ServiceRegistry.all;
const languages = this.services.ServiceRegistry.all as LangiumCombinedServices[];
// TODO @montymxb downcast is not safe, needs additional work
const languages = this.services.ServiceRegistry.all as LangiumCombinedServices[]; // as LangiumCombinedServices[]
const hasFormattingService = this.hasService(e => e.lsp.Formatter);
const formattingOnTypeOptions = languages.map(e => e.lsp.Formatter?.formatOnTypeOptions).find(e => Boolean(e));
const hasCodeActionProvider = this.hasService(e => e.lsp.CodeActionProvider);
Expand Down
6 changes: 6 additions & 0 deletions packages/langium/src/lsp/lsp-services.ts
Expand Up @@ -32,6 +32,9 @@ import type { FuzzyMatcher } from './fuzzy-matcher.js';
import type { Connection, TextDocuments } from 'vscode-languageserver';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { DeepPartial } from '../services.js';
import type { ConfigurationProvider } from './../workspace/configuration.js';
import type { LangiumDocumentFactory } from './../workspace/documents.js';
import type { WorkspaceManager } from './../workspace/workspace-manager.js';

export type LSPServices = {
lsp: {
Expand Down Expand Up @@ -77,7 +80,10 @@ export type LangiumSharedLSPServices = {
LanguageServer: LanguageServer
}
workspace: {
ConfigurationProvider: ConfigurationProvider
LangiumDocumentFactory: LangiumDocumentFactory
TextDocuments: TextDocuments<TextDocument>
WorkspaceManager: WorkspaceManager
}
};

Expand Down
80 changes: 80 additions & 0 deletions packages/langium/src/lsp/workspace/default-configuration.ts
@@ -0,0 +1,80 @@
/******************************************************************************
* 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 { Connection, DidChangeConfigurationParams } from 'vscode-languageserver';
import type { ConfigurationItem } from 'vscode-languageserver-protocol';
import type { ServiceRegistry } from '../../service-registry.js';
import type { LangiumSharedServices } from '../../services.js';
import { DidChangeConfigurationNotification } from 'vscode-languageserver-protocol';
import type { ConfigurationProvider } from '../../workspace/configuration.js';
import type { LangiumSharedLSPServices } from '../lsp-services.js';

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

// TODO needs a block still
export class LSPConfigurationProvider implements ConfigurationProvider {

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

constructor(services: LangiumSharedServices & LangiumSharedLSPServices) {
this.serviceRegistry = services.ServiceRegistry;
this.connection = services.lsp.Connection;
services.lsp.LanguageServer.onInitialize(params => {
this.workspaceConfig = params.capabilities.workspace?.configuration ?? false;
});
services.lsp.LanguageServer.onInitialized(_params => {
const languages = this.serviceRegistry.all;
services.lsp.Connection?.client.register(DidChangeConfigurationNotification.type, {
// Listen to configuration changes for all languages
section: languages.map(lang => this.toSectionName(lang.LanguageMetaData.languageId))
});
});
}

protected 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) }; });
// get workspace configurations (default scope URI)
const configs = await this.connection.workspace.getConfiguration(configToUpdate);
configToUpdate.forEach((conf, idx) => {
this.updateSectionConfiguration(conf.section!, configs[idx]);
});
}
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}`;
}
}
145 changes: 145 additions & 0 deletions packages/langium/src/lsp/workspace/documents.ts
@@ -0,0 +1,145 @@
/******************************************************************************
* 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 { TextDocuments } from 'vscode-languageserver';
import type { ParseResult } from '../../parser/langium-parser.js';
import type { ServiceRegistry } from '../../service-registry.js';
import type { LangiumSharedServices } from '../../services.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 type { LangiumSharedLSPServices } from '../lsp-services.js';

// TODO needs a block still
export class LSPLangiumDocumentFactory implements LangiumDocumentFactory {

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

constructor(services: LangiumSharedServices & LangiumSharedLSPServices) {
this.serviceRegistry = services.ServiceRegistry;
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> {
// 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);
}
}

/**
* 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;
}

update<T extends AstNode = AstNode>(document: Mutable<LangiumDocument<T>>): LangiumDocument<T> {
const textDocument = this.textDocuments.get(document.uri.toString());
const text = textDocument ? textDocument.getText() : this.getContentFromFileSystem(document.uri);

if (textDocument) {
Object.defineProperty(
document, 'textDocument',
{
value: textDocument
}
);
} else {
const textDocumentGetter = this.createTextDocumentGetter(document.uri, text);
Object.defineProperty(
document, 'textDocument',
{
get: textDocumentGetter
}
);
}

document.parseResult = this.parse(document.uri, text);
(document.parseResult.value as Mutable<AstNode>).$document = document;
return document;
}

protected getContentFromFileSystem(uri: URI): string {
return this.fileSystemProvider.readFileSync(uri);
}

protected parse<T extends AstNode>(uri: URI, text: string): ParseResult<T> {
const services = this.serviceRegistry.getServices(uri);
return services.parser.LangiumParser.parse<T>(text);
}

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 ?? ''
);
};
}
}

0 comments on commit 643ff90

Please sign in to comment.