@microsoft/compose-language-service
Version:
Language service for Docker Compose documents
215 lines • 10.1 kB
JavaScript
/*!--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as util from 'util';
import { CancellationToken, ErrorCodes, ResponseError, TextDocuments, TextDocumentSyncKind, } from 'vscode-languageserver';
import { DocumentSettingsNotification } from '../common/DocumentSettingsClientCapabilities';
import { initEvent } from '../common/TelemetryEvent';
import { ComposeDocument } from './ComposeDocument';
import { MultiCompletionProvider } from './providers/completion/MultiCompletionProvider';
import { DiagnosticProvider } from './providers/DiagnosticProvider';
import { DocumentFormattingProvider } from './providers/DocumentFormattingProvider';
import { ImageLinkProvider } from './providers/ImageLinkProvider';
import { KeyHoverProvider } from './providers/KeyHoverProvider';
import { ServiceStartupCodeLensProvider } from './providers/ServiceStartupCodeLensProvider';
import { runWithContext } from './utils/ActionContext';
import { TelemetryAggregator } from './utils/telemetry/TelemetryAggregator';
const DefaultCapabilities = {
// Text document synchronization
textDocumentSync: {
openClose: true,
change: TextDocumentSyncKind.Incremental,
willSave: false,
willSaveWaitUntil: false,
save: false,
},
// Both basic and advanced completions
completionProvider: {
triggerCharacters: ['-', ':', ' ', '"'],
resolveProvider: false,
},
// Code lenses for starting services
codeLensProvider: {
resolveProvider: false,
},
// Hover over YAML keys
hoverProvider: true,
// Links to Docker Hub on image names
documentLinkProvider: {
resolveProvider: false,
},
// YAML formatting
documentFormattingProvider: true,
// Workspace features
workspace: {
workspaceFolders: {
supported: true,
},
},
};
// Default settings for a client with no alternate YAML language service
const DefaultAlternateYamlLanguageServiceClientCapabilities = {
syntaxValidation: false,
schemaValidation: false,
basicCompletions: false,
advancedCompletions: false,
serviceStartupCodeLens: false,
hover: false,
imageLinks: false,
formatting: false,
};
export class ComposeLanguageService {
connection;
clientParams;
documentManager = new TextDocuments(ComposeDocument.DocumentManagerConfig);
subscriptions = [];
telemetryAggregator;
_capabilities = DefaultCapabilities;
constructor(connection, clientParams) {
this.connection = connection;
this.clientParams = clientParams;
let altYamlCapabilities = clientParams.capabilities.experimental?.alternateYamlLanguageService;
if (altYamlCapabilities) {
connection.console.info('An alternate YAML language service is present. The Compose language service will not enable features already provided by the alternate.');
}
else {
altYamlCapabilities = DefaultAlternateYamlLanguageServiceClientCapabilities;
}
// Hook up the document listeners, which create a Disposable which will be added to this.subscriptions
if (altYamlCapabilities.syntaxValidation && altYamlCapabilities.schemaValidation) {
// Noop. No server-side capability needs to be set for diagnostics because it is based on pushing from server to client.
}
else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
this.createDocumentManagerHandler(this.documentManager.onDidChangeContent, new DiagnosticProvider(clientParams.initializationOptions?.diagnosticDelay, !altYamlCapabilities.syntaxValidation, !altYamlCapabilities.schemaValidation));
}
// End of document listeners
// Hook up all the applicable LSP listeners, which do not create Disposables for some reason
if (altYamlCapabilities.basicCompletions && altYamlCapabilities.advancedCompletions) {
this._capabilities.completionProvider = undefined;
}
else {
this.createLspHandler(this.connection.onCompletion.bind(this), new MultiCompletionProvider(!altYamlCapabilities.basicCompletions, !altYamlCapabilities.advancedCompletions));
}
if (altYamlCapabilities.serviceStartupCodeLens) {
this._capabilities.codeLensProvider = undefined;
}
else {
this.createLspHandler(this.connection.onCodeLens.bind(this), new ServiceStartupCodeLensProvider());
}
if (altYamlCapabilities.hover) {
this._capabilities.hoverProvider = undefined;
}
else {
this.createLspHandler(this.connection.onHover.bind(this), new KeyHoverProvider());
}
if (altYamlCapabilities.imageLinks) {
this._capabilities.documentLinkProvider = undefined;
}
else {
this.createLspHandler(this.connection.onDocumentLinks.bind(this), new ImageLinkProvider());
}
if (altYamlCapabilities.formatting) {
this._capabilities.documentFormattingProvider = undefined;
}
else {
this.createLspHandler(this.connection.onDocumentFormatting.bind(this), new DocumentFormattingProvider());
}
// End of LSP listeners
// Hook up one additional notification handler
this.connection.onNotification(DocumentSettingsNotification.type, (params) => this.onDidChangeDocumentSettings(params));
// Start the document listener
this.documentManager.listen(this.connection);
// Start the telemetry aggregator
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
this.subscriptions.push(this.telemetryAggregator = new TelemetryAggregator(this.connection, clientParams.initializationOptions?.telemetryAggregationInterval));
}
dispose() {
for (const subscription of this.subscriptions) {
subscription.dispose();
}
}
get capabilities() {
return this._capabilities;
}
onDidChangeDocumentSettings(params) {
// TODO: Telemetrize this?
const composeDoc = this.documentManager.get(params.textDocument.uri);
if (composeDoc) {
composeDoc.updateSettings(params);
}
}
createLspHandler(event,
// The result (`R`) and error (`E`) types are inferred from the connection's
// handler signature (via `NoInfer`) and the provider is validated against them.
// The partial-result type (`PR`) is not cross-checked: the LSP connection types it
// as the item type while providers use `never`, and those are contravariantly
// incompatible even though the wrapper simply forwards the reporter unchanged.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler) {
event(async (params, token, workDoneProgress, resultProgress) => {
return await this.callWithTelemetryAndErrorHandling(handler.constructor.name, async () => {
const doc = this.documentManager.get(params.textDocument.uri);
if (!doc) {
throw new ResponseError(ErrorCodes.InvalidParams, 'Document not found in cache.');
}
const extendedParams = {
...params,
document: doc,
};
return await Promise.resolve(handler.on(extendedParams, token, workDoneProgress, resultProgress));
});
});
}
createDocumentManagerHandler(event, handler) {
event(async (params) => {
return await this.callWithTelemetryAndErrorHandling(handler.constructor.name, async () => {
const extendedParams = {
...params,
textDocument: params.document.id,
};
return await Promise.resolve(handler.on(extendedParams, CancellationToken.None));
});
}, this, this.subscriptions);
}
async callWithTelemetryAndErrorHandling(callbackId, callback) {
const actionContext = {
clientCapabilities: this.clientParams.capabilities,
connection: this.connection,
telemetry: initEvent(callbackId),
};
const startTime = process.hrtime.bigint();
try {
return await runWithContext(actionContext, callback);
}
catch (error) {
let responseError;
let stack;
if (error instanceof ResponseError) {
responseError = error;
stack = error.stack;
}
else if (error instanceof Error) {
responseError = new ResponseError(ErrorCodes.UnknownErrorCode, error.message, error);
stack = error.stack;
}
else {
responseError = new ResponseError(ErrorCodes.InternalError, util.inspect(error) || 'Unknown error');
}
actionContext.telemetry.properties.result = 'Failed';
actionContext.telemetry.properties.error = responseError.code.toString();
actionContext.telemetry.properties.errorMessage = responseError.message;
actionContext.telemetry.properties.stack = stack;
return responseError;
}
finally {
const endTime = process.hrtime.bigint();
const elapsedMicroseconds = Number((endTime - startTime) / BigInt(1000));
actionContext.telemetry.measurements.duration = elapsedMicroseconds;
// The aggregator will internally handle suppressing / etc.
this.telemetryAggregator.logEvent(actionContext.telemetry);
}
}
}
//# sourceMappingURL=ComposeLanguageService.js.map