From ef4bb6fda4699939be9a04b7a25f836ec010b2f6 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 8 Apr 2024 10:31:26 +0200 Subject: [PATCH] feat: start runner in language server (#1006) ### Summary of Changes The runner is now also started in the language server. The plan is to eventually always use this runner instead of creating a second services object in the VS Code extension. This eases the implementation of language clients for other IDEs. --- .../lsp/safe-ds-inlay-hint-provider.ts | 34 ++----- .../src/language/runner/safe-ds-runner.ts | 29 ++++-- .../src/language/safe-ds-module.ts | 23 ++++- .../validation/builtins/experimental.ts | 20 ++-- .../experimentalLanguageFeatures.ts | 16 +-- .../src/language/validation/names.ts | 4 +- .../src/language/validation/style.ts | 68 ++++++------- .../safe-ds-configuration-provider.ts | 38 ++++++++ .../workspace/safe-ds-settings-provider.ts | 97 ++++++++++++++----- .../safe-ds-settings-provider.test.ts | 75 ++++++++++++++ packages/safe-ds-vscode/package.json | 4 +- .../src/extension/mainClient.ts | 4 +- 12 files changed, 292 insertions(+), 120 deletions(-) create mode 100644 packages/safe-ds-lang/src/language/workspace/safe-ds-configuration-provider.ts create mode 100644 packages/safe-ds-lang/tests/language/workspace/safe-ds-settings-provider.test.ts diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts index 09bee8ffb..f8833dd60 100644 --- a/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts @@ -1,11 +1,5 @@ -import { AstNode, AstUtils, CstNode, DocumentationProvider, interruptAndCheck, LangiumDocument } from 'langium'; -import { - CancellationToken, - type InlayHint, - InlayHintKind, - type InlayHintParams, - MarkupContent, -} from 'vscode-languageserver'; +import { AstNode, AstUtils, CstNode, DocumentationProvider } from 'langium'; +import { InlayHintKind, MarkupContent } from 'vscode-languageserver'; import { createMarkupContent } from '../documentation/safe-ds-comment-provider.js'; import { isSdsArgument, @@ -42,38 +36,22 @@ export class SafeDsInlayHintProvider extends AbstractInlayHintProvider { this.typeComputer = services.typing.TypeComputer; } - override async getInlayHints( - document: LangiumDocument, - params: InlayHintParams, - cancelToken = CancellationToken.None, - ): Promise { - const root = document.parseResult.value; - const inlayHints: InlayHint[] = []; - const acceptor: InlayHintAcceptor = (hint) => inlayHints.push(hint); - for (const node of AstUtils.streamAst(root, { range: params.range })) { - await interruptAndCheck(cancelToken); - // We have to override this method to add this await - await this.computeInlayHint(node, acceptor); - } - return inlayHints; - } - - override async computeInlayHint(node: AstNode, acceptor: InlayHintAcceptor): Promise { + override computeInlayHint(node: AstNode, acceptor: InlayHintAcceptor): void { const cstNode = node.$cstNode; if (!cstNode) { /* c8 ignore next 2 */ return; } - if (await this.settingsProvider.shouldShowAssigneeTypeInlayHints()) { + if (this.settingsProvider.shouldShowAssigneeTypeInlayHints()) { this.computeAssigneeTypeInlayHint(node, cstNode, acceptor); } - if (await this.settingsProvider.shouldShowLambdaParameterTypeInlayHints()) { + if (this.settingsProvider.shouldShowLambdaParameterTypeInlayHints()) { this.computeLambdaParameterTypeInlayHint(node, cstNode, acceptor); } - if (await this.settingsProvider.shouldShowParameterNameInlayHints()) { + if (this.settingsProvider.shouldShowParameterNameInlayHints()) { this.computeParameterNameInlayHint(node, cstNode, acceptor); } } diff --git a/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts b/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts index 3042bb20b..5a941a143 100644 --- a/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts +++ b/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts @@ -49,18 +49,35 @@ export class SafeDsRunner { PipelineExecutionInformation >(); + /* c8 ignore start */ constructor(services: SafeDsServices) { this.annotations = services.builtins.Annotations; this.generator = services.generation.PythonGenerator; this.logging = { - outputError(_value: string) {}, - outputInfo(_value: string) {}, - displayError(_value: string) {}, + outputError(value: string) { + services.lsp.MessagingProvider.error('Runner', value); + }, + outputInfo(value: string) { + services.lsp.MessagingProvider.info('Runner', value); + }, + displayError(value: string) { + services.lsp.MessagingProvider.showErrorMessage(value); + }, }; + + // Register listeners this.registerMessageLoggingCallbacks(); + + services.workspace.SettingsProvider.onRunnerCommandUpdate(async (newValue) => { + this.updateRunnerCommand(newValue); + await this.startPythonServer(); + }); + + services.shared.lsp.Connection?.onShutdown(async () => { + await this.stopPythonServer(); + }); } - /* c8 ignore start */ private registerMessageLoggingCallbacks() { this.addMessageCallback((message) => { this.logging.outputInfo( @@ -71,8 +88,8 @@ export class SafeDsRunner { this.logging.outputInfo( `Placeholder was calculated (${message.id}): ${message.data.name} of type ${message.data.type}`, ); - const execInfo = this.getExecutionContext(message.id)!; - execInfo.calculatedPlaceholders.set(message.data.name, message.data.type); + const execInfo = this.getExecutionContext(message.id); + execInfo?.calculatedPlaceholders.set(message.data.name, message.data.type); // this.sendMessageToPythonServer( // messages.createPlaceholderQueryMessage(message.id, message.data.name), //); diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts index 608aadf98..6957f48eb 100644 --- a/packages/safe-ds-lang/src/language/safe-ds-module.ts +++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts @@ -46,6 +46,7 @@ import { SafeDsMarkdownGenerator } from './generation/safe-ds-markdown-generator import { SafeDsCompletionProvider } from './lsp/safe-ds-completion-provider.js'; import { SafeDsFuzzyMatcher } from './lsp/safe-ds-fuzzy-matcher.js'; import { SafeDsMessagingProvider } from './lsp/safe-ds-messaging-provider.js'; +import { SafeDsConfigurationProvider } from './workspace/safe-ds-configuration-provider.js'; /** * Declaration of custom services - add your own service classes here. @@ -96,11 +97,22 @@ export type SafeDsAddedServices = { }; }; +export type SafeDsAddedSharedServices = { + workspace: { + ConfigurationProvider: SafeDsConfigurationProvider; + }; +}; + /** * Union of Langium default services and your custom services - use this as constructor parameter * of custom service classes. */ -export type SafeDsServices = LangiumServices & SafeDsAddedServices; +export type SafeDsServices = LangiumServices & + SafeDsAddedServices & { + shared: SafeDsAddedSharedServices; + }; + +export type SafeDsSharedServices = LangiumSharedServices & SafeDsAddedSharedServices; /** * Dependency injection module that overrides Langium default services and contributes the @@ -170,14 +182,13 @@ export const SafeDsModule: Module> = { lsp: { FuzzyMatcher: () => new SafeDsFuzzyMatcher(), NodeKindProvider: () => new SafeDsNodeKindProvider(), }, workspace: { + ConfigurationProvider: (services) => new SafeDsConfigurationProvider(services), DocumentBuilder: (services) => new SafeDsDocumentBuilder(services), WorkspaceManager: (services) => new SafeDsWorkspaceManager(services), }, @@ -206,7 +217,11 @@ export const createSafeDsServices = async function ( shared: LangiumSharedServices; SafeDs: SafeDsServices; }> { - const shared = inject(createDefaultSharedModule(context), SafeDsGeneratedSharedModule, SafeDsSharedModule); + const shared: SafeDsSharedServices = inject( + createDefaultSharedModule(context), + SafeDsGeneratedSharedModule, + SafeDsSharedModule, + ); const SafeDs = inject(createDefaultModule({ shared }), SafeDsGeneratedModule, SafeDsModule); shared.ServiceRegistry.register(SafeDs); diff --git a/packages/safe-ds-lang/src/language/validation/builtins/experimental.ts b/packages/safe-ds-lang/src/language/validation/builtins/experimental.ts index cfe320831..8250140c3 100644 --- a/packages/safe-ds-lang/src/language/validation/builtins/experimental.ts +++ b/packages/safe-ds-lang/src/language/validation/builtins/experimental.ts @@ -16,8 +16,8 @@ export const CODE_EXPERIMENTAL_LIBRARY_ELEMENT = 'experimental/library-element'; export const assigneeAssignedResultShouldNotBeExperimental = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAssignee, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLibraryElements())) { + return (node: SdsAssignee, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLibraryElements()) { /* c8 ignore next 2 */ return; } @@ -43,8 +43,8 @@ export const assigneeAssignedResultShouldNotBeExperimental = (services: SafeDsSe export const annotationCallAnnotationShouldNotBeExperimental = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAnnotationCall, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLibraryElements())) { + return (node: SdsAnnotationCall, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLibraryElements()) { /* c8 ignore next 2 */ return; } @@ -67,8 +67,8 @@ export const annotationCallAnnotationShouldNotBeExperimental = (services: SafeDs export const argumentCorrespondingParameterShouldNotBeExperimental = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsArgument, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLibraryElements())) { + return (node: SdsArgument, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLibraryElements()) { /* c8 ignore next 2 */ return; } @@ -90,8 +90,8 @@ export const argumentCorrespondingParameterShouldNotBeExperimental = (services: export const namedTypeDeclarationShouldNotBeExperimental = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsNamedType, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLibraryElements())) { + return (node: SdsNamedType, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLibraryElements()) { /* c8 ignore next 2 */ return; } @@ -113,8 +113,8 @@ export const namedTypeDeclarationShouldNotBeExperimental = (services: SafeDsServ export const referenceTargetShouldNotExperimental = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsReference, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLibraryElements())) { + return (node: SdsReference, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLibraryElements()) { /* c8 ignore next 2 */ return; } diff --git a/packages/safe-ds-lang/src/language/validation/experimentalLanguageFeatures.ts b/packages/safe-ds-lang/src/language/validation/experimentalLanguageFeatures.ts index ac0b22e52..8ad11d85b 100644 --- a/packages/safe-ds-lang/src/language/validation/experimentalLanguageFeatures.ts +++ b/packages/safe-ds-lang/src/language/validation/experimentalLanguageFeatures.ts @@ -14,8 +14,8 @@ export const CODE_EXPERIMENTAL_LANGUAGE_FEATURE = 'experimental/language-feature export const constraintListsShouldBeUsedWithCaution = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsConstraintList, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLanguageFeatures())) { + return (node: SdsConstraintList, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLanguageFeatures()) { /* c8 ignore next 2 */ return; } @@ -31,8 +31,8 @@ export const constraintListsShouldBeUsedWithCaution = (services: SafeDsServices) export const literalTypesShouldBeUsedWithCaution = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsLiteralType, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLanguageFeatures())) { + return (node: SdsLiteralType, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLanguageFeatures()) { /* c8 ignore next 2 */ return; } @@ -48,8 +48,8 @@ export const literalTypesShouldBeUsedWithCaution = (services: SafeDsServices) => export const mapsShouldBeUsedWithCaution = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsMap, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLanguageFeatures())) { + return (node: SdsMap, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLanguageFeatures()) { /* c8 ignore next 2 */ return; } @@ -69,8 +69,8 @@ export const mapsShouldBeUsedWithCaution = (services: SafeDsServices) => { export const unionTypesShouldBeUsedWithCaution = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsUnionType, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateExperimentalLanguageFeatures())) { + return (node: SdsUnionType, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateExperimentalLanguageFeatures()) { /* c8 ignore next 2 */ return; } diff --git a/packages/safe-ds-lang/src/language/validation/names.ts b/packages/safe-ds-lang/src/language/validation/names.ts index 22423d762..01aef9278 100644 --- a/packages/safe-ds-lang/src/language/validation/names.ts +++ b/packages/safe-ds-lang/src/language/validation/names.ts @@ -105,8 +105,8 @@ export const nameMustNotOccurOnCoreDeclaration = (services: SafeDsServices) => { export const nameShouldHaveCorrectCasing = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsDeclaration, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateNameConvention())) { + return (node: SdsDeclaration, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateNameConvention()) { /* c8 ignore next 2 */ return; } diff --git a/packages/safe-ds-lang/src/language/validation/style.ts b/packages/safe-ds-lang/src/language/validation/style.ts index d308180f5..4ebc1c944 100644 --- a/packages/safe-ds-lang/src/language/validation/style.ts +++ b/packages/safe-ds-lang/src/language/validation/style.ts @@ -49,8 +49,8 @@ export const CODE_STYLE_UNNECESSARY_UNION_TYPE = 'style/unnecessary-union-type'; export const annotationCallArgumentListShouldBeNeeded = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAnnotationCall, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsAnnotationCall, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -79,8 +79,8 @@ export const annotationCallArgumentListShouldBeNeeded = (services: SafeDsService export const callArgumentListShouldBeNeeded = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsCall, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsCall, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -112,8 +112,8 @@ export const callArgumentListShouldBeNeeded = (services: SafeDsServices) => { export const assignmentShouldHaveMoreThanWildcardsAsAssignees = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAssignment, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsAssignment, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -135,8 +135,8 @@ export const assignmentShouldHaveMoreThanWildcardsAsAssignees = (services: SafeD export const classBodyShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsClassBody, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsClassBody, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -153,8 +153,8 @@ export const classBodyShouldNotBeEmpty = (services: SafeDsServices) => { export const enumBodyShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsEnumBody, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsEnumBody, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -175,8 +175,8 @@ export const enumBodyShouldNotBeEmpty = (services: SafeDsServices) => { export const annotationParameterShouldNotHaveConstModifier = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAnnotation, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsAnnotation, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -200,8 +200,8 @@ export const annotationParameterShouldNotHaveConstModifier = (services: SafeDsSe export const constraintListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsConstraintList, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsConstraintList, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -225,8 +225,8 @@ export const elvisOperatorShouldBeNeeded = (services: SafeDsServices) => { const typeChecker = services.typing.TypeChecker; const typeComputer = services.typing.TypeComputer; - return async (node: SdsInfixOperation, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsInfixOperation, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -287,8 +287,8 @@ export const elvisOperatorShouldBeNeeded = (services: SafeDsServices) => { export const importedDeclarationAliasShouldDifferFromDeclarationName = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsImportedDeclaration, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsImportedDeclaration, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -312,8 +312,8 @@ export const chainedExpressionNullSafetyShouldBeNeeded = (services: SafeDsServic const typeChecker = services.typing.TypeChecker; const typeComputer = services.typing.TypeComputer; - return async (node: SdsChainedExpression, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsChainedExpression, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -347,8 +347,8 @@ export const chainedExpressionNullSafetyShouldBeNeeded = (services: SafeDsServic export const annotationParameterListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsAnnotation, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsAnnotation, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -366,8 +366,8 @@ export const annotationParameterListShouldNotBeEmpty = (services: SafeDsServices export const enumVariantParameterListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsEnumVariant, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsEnumVariant, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -389,8 +389,8 @@ export const enumVariantParameterListShouldNotBeEmpty = (services: SafeDsService export const functionResultListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsFunction, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsFunction, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -408,8 +408,8 @@ export const functionResultListShouldNotBeEmpty = (services: SafeDsServices) => export const segmentResultListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsSegment, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsSegment, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -431,8 +431,8 @@ export const segmentResultListShouldNotBeEmpty = (services: SafeDsServices) => { export const namedTypeTypeArgumentListShouldBeNeeded = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsNamedType, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsNamedType, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -464,8 +464,8 @@ export const namedTypeTypeArgumentListShouldBeNeeded = (services: SafeDsServices export const typeParameterListShouldNotBeEmpty = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsTypeParameterList, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsTypeParameterList, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } @@ -486,8 +486,8 @@ export const typeParameterListShouldNotBeEmpty = (services: SafeDsServices) => { export const unionTypeShouldNotHaveASingularTypeArgument = (services: SafeDsServices) => { const settingsProvider = services.workspace.SettingsProvider; - return async (node: SdsUnionType, accept: ValidationAcceptor) => { - if (!(await settingsProvider.shouldValidateCodeStyle())) { + return (node: SdsUnionType, accept: ValidationAcceptor) => { + if (!settingsProvider.shouldValidateCodeStyle()) { /* c8 ignore next 2 */ return; } diff --git a/packages/safe-ds-lang/src/language/workspace/safe-ds-configuration-provider.ts b/packages/safe-ds-lang/src/language/workspace/safe-ds-configuration-provider.ts new file mode 100644 index 000000000..a8fa96ae7 --- /dev/null +++ b/packages/safe-ds-lang/src/language/workspace/safe-ds-configuration-provider.ts @@ -0,0 +1,38 @@ +import { DefaultConfigurationProvider, Emitter, Event } from 'langium'; +import { DidChangeConfigurationParams } from 'vscode-languageserver'; + +export class SafeDsConfigurationProvider extends DefaultConfigurationProvider { + protected onConfigurationSectionUpdateEmitter = new Emitter(); + + override updateConfiguration(change: DidChangeConfigurationParams): void { + if (!change.settings) { + /* c8 ignore next 2 */ + return; + } + + Object.keys(change.settings).forEach((section) => { + const configuration = change.settings[section]; + this.updateSectionConfiguration(section, configuration); + this.onConfigurationSectionUpdateEmitter.fire({ section, configuration }); + }); + } + + /** + * Get notified after a configuration section has been updated. + */ + get onConfigurationSectionUpdate(): Event { + return this.onConfigurationSectionUpdateEmitter.event; + } +} + +type ConfigurationSectionUpdate = { + /** + * The name of the configuration section that has been updated. + */ + section: string; + + /** + * The updated configuration section. + */ + configuration: any; +}; diff --git a/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts b/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts index 583ff91d3..a313fcd50 100644 --- a/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts +++ b/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts @@ -1,55 +1,100 @@ -import { ConfigurationProvider } from 'langium'; import { SafeDsServices } from '../safe-ds-module.js'; import { SafeDsLanguageMetaData } from '../generated/module.js'; +import { SafeDsConfigurationProvider } from './safe-ds-configuration-provider.js'; +import { DeepPartial, Disposable } from 'langium'; export class SafeDsSettingsProvider { - private readonly configurationProvider: ConfigurationProvider; + private readonly configurationProvider: SafeDsConfigurationProvider; + + private cachedSettings: DeepPartial = {}; + private watchers = new Set>(); constructor(services: SafeDsServices) { this.configurationProvider = services.shared.workspace.ConfigurationProvider; + + /* c8 ignore start */ + this.configurationProvider.onConfigurationSectionUpdate(async ({ section, configuration }) => { + if (section === SafeDsLanguageMetaData.languageId) { + await this.updateCachedSettings(configuration); + } + }); + /* c8 ignore stop */ } - async shouldShowAssigneeTypeInlayHints(): Promise { - return (await this.getInlayHintsSettings()).assigneeTypes?.enabled ?? true; + shouldShowAssigneeTypeInlayHints(): boolean { + return this.cachedSettings.inlayHints?.assigneeTypes?.enabled ?? true; } - async shouldShowLambdaParameterTypeInlayHints(): Promise { - return (await this.getInlayHintsSettings()).lambdaParameterTypes?.enabled ?? true; + shouldShowLambdaParameterTypeInlayHints(): boolean { + return this.cachedSettings.inlayHints?.lambdaParameterTypes?.enabled ?? true; } - async shouldShowParameterNameInlayHints(): Promise { - return (await this.getInlayHintsSettings()).parameterNames?.enabled ?? true; + shouldShowParameterNameInlayHints(): boolean { + return this.cachedSettings.inlayHints?.parameterNames?.enabled ?? true; } - private async getInlayHintsSettings(): Promise> { - return ( - (await this.configurationProvider.getConfiguration(SafeDsLanguageMetaData.languageId, 'inlayHints')) ?? {} - ); + /* c8 ignore start */ + getRunnerCommand(): string { + return this.cachedSettings.runner?.command ?? 'safe-ds-runner'; } + /* c8 ignore stop */ + + onRunnerCommandUpdate(callback: (newValue: string | undefined) => void): Disposable { + const watcher: SettingsWatcher = { + accessor: (settings) => settings.runner?.command, + callback, + }; + + this.watchers.add(watcher); - async shouldValidateCodeStyle(): Promise { - return (await this.getValidationSettings()).codeStyle?.enabled ?? true; + return Disposable.create(() => { + /* c8 ignore next */ + this.watchers.delete(watcher); + }); } - async shouldValidateExperimentalLanguageFeatures(): Promise { - return (await this.getValidationSettings()).experimentalLanguageFeatures?.enabled ?? true; + shouldValidateCodeStyle(): boolean { + return this.cachedSettings.validation?.codeStyle?.enabled ?? true; } - async shouldValidateExperimentalLibraryElements(): Promise { - return (await this.getValidationSettings()).experimentalLibraryElements?.enabled ?? true; + shouldValidateExperimentalLanguageFeatures(): boolean { + return this.cachedSettings.validation?.experimentalLanguageFeatures?.enabled ?? true; } - async shouldValidateNameConvention(): Promise { - return (await this.getValidationSettings()).nameConvention?.enabled ?? true; + shouldValidateExperimentalLibraryElements(): boolean { + return this.cachedSettings.validation?.experimentalLibraryElements?.enabled ?? true; } - private async getValidationSettings(): Promise> { - return ( - (await this.configurationProvider.getConfiguration(SafeDsLanguageMetaData.languageId, 'validation')) ?? {} - ); + shouldValidateNameConvention(): boolean { + return this.cachedSettings.validation?.nameConvention?.enabled ?? true; + } + + private async updateCachedSettings(newSettings: Settings): Promise { + const oldSettings = this.cachedSettings; + this.cachedSettings = newSettings; + + // Notify watchers + for (const watcher of this.watchers) { + const oldValue = watcher.accessor(oldSettings); + const newValue = watcher.accessor(this.cachedSettings); + if (oldValue !== newValue) { + watcher.callback(newValue); + } + } } } +interface SettingsWatcher { + accessor: (settings: DeepPartial) => T; + callback: (newValue: T) => void; +} + +interface Settings { + inlayHints: InlayHintsSettings; + runner: RunnerSettings; + validation: ValidationSettings; +} + interface InlayHintsSettings { assigneeTypes: { enabled: boolean; @@ -62,6 +107,10 @@ interface InlayHintsSettings { }; } +interface RunnerSettings { + command: string; +} + interface ValidationSettings { codeStyle: { enabled: boolean; diff --git a/packages/safe-ds-lang/tests/language/workspace/safe-ds-settings-provider.test.ts b/packages/safe-ds-lang/tests/language/workspace/safe-ds-settings-provider.test.ts new file mode 100644 index 000000000..12f3a32f2 --- /dev/null +++ b/packages/safe-ds-lang/tests/language/workspace/safe-ds-settings-provider.test.ts @@ -0,0 +1,75 @@ +import { NodeFileSystem } from 'langium/node'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createSafeDsServices } from '../../../src/language/index.js'; + +const services = (await createSafeDsServices(NodeFileSystem)).SafeDs; +const configurationProvider = services.shared.workspace.ConfigurationProvider; +const languageId = services.LanguageMetaData.languageId; +const settingsProvider = services.workspace.SettingsProvider; + +describe('SafeDsSettingsProvider', () => { + describe('onRunnerCommandUpdate', () => { + beforeEach(() => { + configurationProvider.updateConfiguration({ + settings: { + [languageId]: { + runner: { + command: 'safe-ds-runner', + }, + }, + }, + }); + }); + + it('should call the callback with the new value if it differs from the old value', () => { + const callback = vi.fn(); + const disposable = settingsProvider.onRunnerCommandUpdate(callback); + configurationProvider.updateConfiguration({ + settings: { + [languageId]: { + runner: { + command: 'safe-ds-runner-2', + }, + }, + }, + }); + expect(callback).toHaveBeenCalledWith('safe-ds-runner-2'); + disposable.dispose(); + }); + + it('should not call the callback if the new value is the same as the old value', () => { + const callback = vi.fn(); + const disposable = settingsProvider.onRunnerCommandUpdate(callback); + + configurationProvider.updateConfiguration({ + settings: { + [languageId]: { + runner: { + command: 'safe-ds-runner', + }, + }, + }, + }); + expect(callback).not.toHaveBeenCalled(); + disposable.dispose(); + }); + + it('should not call disposed callbacks', () => { + const callback = vi.fn(); + const disposable = settingsProvider.onRunnerCommandUpdate(callback); + disposable.dispose(); + + configurationProvider.updateConfiguration({ + settings: { + [languageId]: { + runner: { + command: 'safe-ds-runner-2', + }, + }, + }, + }); + + expect(callback).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/safe-ds-vscode/package.json b/packages/safe-ds-vscode/package.json index 21d5493a4..3cd6eab14 100644 --- a/packages/safe-ds-vscode/package.json +++ b/packages/safe-ds-vscode/package.json @@ -99,9 +99,9 @@ "description": "Show parameter names for positional arguments." }, "safe-ds.runner.command": { - "description": "Command to start the Safe-DS runner", "type": "string", - "default": "safe-ds-runner" + "default": "safe-ds-runner", + "description": "Command to start the Safe-DS runner" }, "safe-ds.trace.server": { "scope": "window", diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index 04874dce9..4bd296952 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -4,7 +4,7 @@ import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; import { ast, createSafeDsServices, getModuleMembers, messages, SafeDsServices } from '@safe-ds/lang'; import { NodeFileSystem } from 'langium/node'; -import { getSafeDSOutputChannel, initializeLog, logError, logOutput, printOutputMessage } from './output.js'; +import { initializeLog, logError, logOutput, printOutputMessage } from './output.js'; import crypto from 'crypto'; import { AstNode, AstUtils, LangiumDocument } from 'langium'; import { EDAPanel } from './eda/edaPanel.ts'; @@ -79,7 +79,7 @@ const startLanguageClient = function (context: vscode.ExtensionContext): Languag // Notify the server about file changes to files contained in the workspace fileEvents: fileSystemWatcher, }, - outputChannel: getSafeDSOutputChannel('[LanguageClient] '), + outputChannel: vscode.window.createOutputChannel('Safe-DS Language Client', 'log'), }; // Create the language client and start the client.