UNPKG

dockerfile-language-server-nodejs

Version:

A language server for Dockerfiles powered by NodeJS, TypeScript, and VSCode technologies.

677 lines (676 loc) 28.9 kB
/* -------------------------------------------------------------------------------------------- * Copyright (c) Remy Suen. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const node_1 = require("vscode-languageserver/node"); const files_1 = require("vscode-languageserver/lib/node/files"); const dockerfile_utils_1 = require("dockerfile-utils"); const dockerfile_language_service_1 = require("dockerfile-language-service"); let formatterConfiguration = null; /** * The settings to use for the validator if the client doesn't support * workspace/configuration requests. */ let validatorSettings = null; const formatterConfigurations = new Map(); /** * The validator settings that correspond to an individual file retrieved via * the workspace/configuration request. */ let validatorConfigurations = new Map(); let connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all); let service = dockerfile_language_service_1.DockerfileLanguageServiceFactory.createLanguageService(); service.setLogger({ log(message) { connection.console.log(message); } }); /** * Whether the client supports the workspace/applyEdit request. */ let applyEditSupport = false; /** * Whether the client supports the workspace/configuration request. */ let configurationSupport = false; let documentChangesSupport = false; let codeActionQuickFixSupport = false; let documents = {}; /** * Retrieves a text document for the file located at the given URI * string. * * @param uri the URI of the interested file, must be defined and not * null * @return the text document for the file, or null if no file exists * at the given location */ function getDocument(uri) { if (documents[uri]) { return Promise.resolve(documents[uri]); } return new Promise((resolve, reject) => { let file = (0, files_1.uriToFilePath)(uri); if (file === undefined) { resolve(null); } else { fs.exists(file, (exists) => { if (exists) { fs.readFile(file, (err, data) => { resolve(vscode_languageserver_textdocument_1.TextDocument.create(uri, "dockerfile", 1, data.toString())); }); } else { resolve(null); } }); } }); } function supportsDeprecatedItems(capabilities) { return capabilities.textDocument && capabilities.textDocument.completion && capabilities.textDocument.completion.completionItem && capabilities.textDocument.completion.completionItem.deprecatedSupport; } function supportsSnippets(capabilities) { return capabilities.textDocument && capabilities.textDocument.completion && capabilities.textDocument.completion.completionItem && capabilities.textDocument.completion.completionItem.snippetSupport; } function supportsCodeActionQuickFixes(capabilities) { let values = capabilities.textDocument && capabilities.textDocument.codeAction && capabilities.textDocument.codeAction.codeActionLiteralSupport && capabilities.textDocument.codeAction.codeActionLiteralSupport.codeActionKind && capabilities.textDocument.codeAction.codeActionLiteralSupport.codeActionKind.valueSet; if (values === null || values === undefined) { return false; } for (let value of values) { if (value === node_1.CodeActionKind.QuickFix) { return true; } } return false; } /** * Gets the MarkupKind[] that the client supports for the * documentation field of a CompletionItem. * * @return the supported MarkupKind[], may be null or undefined */ function getCompletionItemDocumentationFormat(capabilities) { return capabilities.textDocument && capabilities.textDocument.completion && capabilities.textDocument.completion.completionItem && capabilities.textDocument.completion.completionItem.documentationFormat; } function getHoverContentFormat(capabilities) { return capabilities.textDocument && capabilities.textDocument.hover && capabilities.textDocument.hover.contentFormat; } function getLineFoldingOnly(capabilities) { return capabilities.textDocument && capabilities.textDocument.foldingRange && capabilities.textDocument.foldingRange.lineFoldingOnly; } function getRangeLimit(capabilities) { let rangeLimit = capabilities.textDocument && capabilities.textDocument.foldingRange && capabilities.textDocument.foldingRange.rangeLimit; if (rangeLimit === null || rangeLimit === undefined || typeof rangeLimit === "boolean" || isNaN(rangeLimit)) { rangeLimit = Number.MAX_VALUE; } else if (typeof rangeLimit !== "number") { // isNaN === false and not a number, must be a string number, convert it rangeLimit = Number(rangeLimit); } return rangeLimit; } function setServiceCapabilities(capabilities) { service.setCapabilities({ completion: { completionItem: { deprecatedSupport: supportsDeprecatedItems(capabilities), documentationFormat: getCompletionItemDocumentationFormat(capabilities), snippetSupport: supportsSnippets(capabilities) } }, hover: { contentFormat: getHoverContentFormat(capabilities) }, foldingRange: { lineFoldingOnly: getLineFoldingOnly(capabilities), rangeLimit: getRangeLimit(capabilities) } }); } connection.onInitialized(() => { if (configurationSupport) { // listen for notification changes if the client supports workspace/configuration connection.client.register(node_1.DidChangeConfigurationNotification.type); } }); connection.onInitialize((params) => { setServiceCapabilities(params.capabilities); applyEditSupport = params.capabilities.workspace && params.capabilities.workspace.applyEdit === true; documentChangesSupport = params.capabilities.workspace && params.capabilities.workspace.workspaceEdit && params.capabilities.workspace.workspaceEdit.documentChanges === true; configurationSupport = params.capabilities.workspace && params.capabilities.workspace.configuration === true; const renamePrepareSupport = params.capabilities.textDocument && params.capabilities.textDocument.rename && params.capabilities.textDocument.rename.prepareSupport === true; const semanticTokensSupport = params.capabilities.textDocument && params.capabilities.textDocument.semanticTokens; codeActionQuickFixSupport = supportsCodeActionQuickFixes(params.capabilities); return { capabilities: { textDocumentSync: node_1.TextDocumentSyncKind.Incremental, codeActionProvider: applyEditSupport, completionProvider: { resolveProvider: true, triggerCharacters: [ '=', ' ', '$', '-', ] }, executeCommandProvider: applyEditSupport ? { commands: [ dockerfile_language_service_1.CommandIds.LOWERCASE, dockerfile_language_service_1.CommandIds.UPPERCASE, dockerfile_language_service_1.CommandIds.EXTRA_ARGUMENT, dockerfile_language_service_1.CommandIds.DIRECTIVE_TO_BACKSLASH, dockerfile_language_service_1.CommandIds.DIRECTIVE_TO_BACKTICK, dockerfile_language_service_1.CommandIds.FLAG_TO_CHOWN, dockerfile_language_service_1.CommandIds.FLAG_TO_COPY_FROM, dockerfile_language_service_1.CommandIds.FLAG_TO_HEALTHCHECK_INTERVAL, dockerfile_language_service_1.CommandIds.FLAG_TO_HEALTHCHECK_RETRIES, dockerfile_language_service_1.CommandIds.FLAG_TO_HEALTHCHECK_START_PERIOD, dockerfile_language_service_1.CommandIds.FLAG_TO_HEALTHCHECK_TIMEOUT, dockerfile_language_service_1.CommandIds.CONVERT_TO_AS, dockerfile_language_service_1.CommandIds.REMOVE_EMPTY_CONTINUATION_LINE ] } : undefined, documentFormattingProvider: true, documentRangeFormattingProvider: true, documentOnTypeFormattingProvider: { firstTriggerCharacter: '\\', moreTriggerCharacter: ['`'] }, hoverProvider: true, documentSymbolProvider: true, documentHighlightProvider: true, renameProvider: renamePrepareSupport ? { prepareProvider: true } : true, definitionProvider: true, signatureHelpProvider: { triggerCharacters: [ '-', '[', ',', ' ', '=' ] }, documentLinkProvider: { resolveProvider: true }, semanticTokensProvider: semanticTokensSupport ? { full: { delta: false }, legend: { tokenTypes: [ node_1.SemanticTokenTypes.keyword, node_1.SemanticTokenTypes.comment, node_1.SemanticTokenTypes.parameter, node_1.SemanticTokenTypes.property, node_1.SemanticTokenTypes.namespace, node_1.SemanticTokenTypes.class, node_1.SemanticTokenTypes.macro, node_1.SemanticTokenTypes.string, node_1.SemanticTokenTypes.variable, node_1.SemanticTokenTypes.operator ], tokenModifiers: [ node_1.SemanticTokenModifiers.declaration, node_1.SemanticTokenModifiers.definition, node_1.SemanticTokenModifiers.deprecated ] } } : undefined, foldingRangeProvider: true } }; }); function convertValidatorConfiguration(config) { let deprecatedMaintainer = dockerfile_utils_1.ValidationSeverity.WARNING; let directiveCasing = dockerfile_utils_1.ValidationSeverity.WARNING; let emptyContinuationLine = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionCasing = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionCmdMultiple = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionEntrypointMultiple = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionHealthcheckMultiple = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionJSONInSingleQuotes = dockerfile_utils_1.ValidationSeverity.WARNING; let instructionWorkdirRelative = dockerfile_utils_1.ValidationSeverity.WARNING; if (config) { deprecatedMaintainer = getSeverity(config.deprecatedMaintainer); directiveCasing = getSeverity(config.directiveCasing); emptyContinuationLine = getSeverity(config.emptyContinuationLine); instructionCasing = getSeverity(config.instructionCasing); instructionCmdMultiple = getSeverity(config.instructionCmdMultiple); instructionEntrypointMultiple = getSeverity(config.instructionEntrypointMultiple); instructionHealthcheckMultiple = getSeverity(config.instructionHealthcheckMultiple); instructionJSONInSingleQuotes = getSeverity(config.instructionJSONInSingleQuotes); instructionWorkdirRelative = getSeverity(config.instructionWorkdirRelative); } return { deprecatedMaintainer, directiveCasing, emptyContinuationLine, instructionCasing, instructionCmdMultiple, instructionEntrypointMultiple, instructionHealthcheckMultiple, instructionJSONInSingleQuotes, instructionWorkdirRelative }; } function validateTextDocument(textDocument) { if (configurationSupport) { getValidatorConfiguration(textDocument.uri).then((config) => { const fileSettings = convertValidatorConfiguration(config); const diagnostics = service.validate(textDocument.getText(), fileSettings); connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); }); } else { const diagnostics = service.validate(textDocument.getText(), validatorSettings); connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } } function getSeverity(severity) { switch (severity) { case "ignore": return dockerfile_utils_1.ValidationSeverity.IGNORE; case "warning": return dockerfile_utils_1.ValidationSeverity.WARNING; case "error": return dockerfile_utils_1.ValidationSeverity.ERROR; } return null; } function getFormatterConfiguration(resource) { let result = formatterConfigurations.get(resource); if (!result) { result = connection.workspace.getConfiguration({ section: "docker.languageserver.formatter", scopeUri: resource }); formatterConfigurations.set(resource, result); } return result; } /** * Gets the validation configuration that pertains to the specified resource. * * @param resource the interested resource * @return the configuration to use to validate the interested resource */ function getValidatorConfiguration(resource) { let result = validatorConfigurations.get(resource); if (!result) { result = connection.workspace.getConfiguration({ section: "docker.languageserver.diagnostics", scopeUri: resource }); validatorConfigurations.set(resource, result); } return result; } // listen for notifications when the client's configuration has changed connection.onNotification(node_1.DidChangeConfigurationNotification.type, () => { refreshConfigurations(); }); function getConfigurationItems(sectionName) { // store all the URIs that need to be refreshed const configurationItems = []; for (const uri in documents) { configurationItems.push({ section: sectionName, scopeUri: uri }); } return configurationItems; } function refreshFormatterConfigurations() { // store all the URIs that need to be refreshed const settingsRequest = getConfigurationItems("docker.languageserver.formatter"); // clear the cache formatterConfigurations.clear(); // ask the workspace for the configurations connection.workspace.getConfiguration(settingsRequest).then((settings) => { for (let i = 0; i < settings.length; i++) { const resource = settingsRequest[i].scopeUri; // a value might have been stored already, use it instead and ignore this one if so if (settings[i] && !formatterConfigurations.has(resource)) { formatterConfigurations.set(resource, Promise.resolve(settings[i])); } } }); } function refreshValidatorConfigurations() { // store all the URIs that need to be refreshed const settingsRequest = getConfigurationItems("docker.languageserver.diagnostics"); // clear the cache validatorConfigurations.clear(); // ask the workspace for the configurations connection.workspace.getConfiguration(settingsRequest).then((values) => { const toRevalidate = []; for (let i = 0; i < values.length; i++) { const resource = settingsRequest[i].scopeUri; // a value might have been stored already, use it instead and ignore this one if so if (values[i] && !validatorConfigurations.has(resource)) { validatorConfigurations.set(resource, Promise.resolve(values[i])); toRevalidate.push(resource); } } for (const resource of toRevalidate) { validateTextDocument(documents[resource]); } }); } /** * Wipes and reloads the internal cache of configurations. */ function refreshConfigurations() { refreshFormatterConfigurations(); refreshValidatorConfigurations(); } connection.onDidChangeConfiguration((change) => { if (configurationSupport) { refreshConfigurations(); } else { let settings = change.settings; if (settings.docker && settings.docker.languageserver) { if (settings.docker.languageserver.diagnostics) { validatorSettings = convertValidatorConfiguration(settings.docker.languageserver.diagnostics); } if (settings.docker.languageserver.formatter) { formatterConfiguration = settings.docker.languageserver.formatter; } } else { formatterConfiguration = null; validatorSettings = convertValidatorConfiguration(null); } // validate all the documents again Object.keys(documents).forEach((key) => { validateTextDocument(documents[key]); }); } }); connection.onCompletion((textDocumentPosition) => { return getDocument(textDocumentPosition.textDocument.uri).then((document) => { if (document) { return service.computeCompletionItems(document.getText(), textDocumentPosition.position); } return null; }); }); connection.onSignatureHelp((textDocumentPosition) => { return getDocument(textDocumentPosition.textDocument.uri).then((document) => { if (document !== null) { return service.computeSignatureHelp(document.getText(), textDocumentPosition.position); } return { signatures: [], activeSignature: null, activeParameter: null, }; }); }); connection.onCompletionResolve((item) => { return service.resolveCompletionItem(item); }); connection.onHover((textDocumentPosition) => { return getDocument(textDocumentPosition.textDocument.uri).then((document) => { if (document) { return service.computeHover(document.getText(), textDocumentPosition.position); } return null; }); }); connection.onDocumentHighlight((textDocumentPosition) => { return getDocument(textDocumentPosition.textDocument.uri).then((document) => { if (document) { return service.computeHighlightRanges(document.getText(), textDocumentPosition.position); } return []; }); }); connection.onCodeAction((codeActionParams) => { if (applyEditSupport && codeActionParams.context.diagnostics.length > 0) { let commands = service.computeCodeActions(codeActionParams.textDocument, codeActionParams.range, codeActionParams.context); if (codeActionQuickFixSupport) { return getDocument(codeActionParams.textDocument.uri).then((document) => { let codeActions = []; for (let command of commands) { let codeAction = { title: command.title, kind: node_1.CodeActionKind.QuickFix }; let edit = computeWorkspaceEdit(codeActionParams.textDocument.uri, document, command.command, command.arguments); if (edit) { codeAction.edit = edit; } codeActions.push(codeAction); } return codeActions; }); } return commands; } return []; }); function computeWorkspaceEdit(uri, document, command, args) { let edits = service.computeCommandEdits(document.getText(), command, args); if (edits) { if (documentChangesSupport) { let identifier = node_1.VersionedTextDocumentIdentifier.create(uri, document.version); return { documentChanges: [ node_1.TextDocumentEdit.create(identifier, edits) ] }; } else { return { changes: { [uri]: edits } }; } } return null; } connection.onExecuteCommand((params) => { if (applyEditSupport) { let uri = params.arguments[0]; getDocument(uri).then((document) => { if (document) { let workspaceEdit = computeWorkspaceEdit(uri, document, params.command, params.arguments); if (workspaceEdit) { connection.workspace.applyEdit(workspaceEdit); } } return null; }); } }); connection.onDefinition((textDocumentPosition) => { return getDocument(textDocumentPosition.textDocument.uri).then((document) => { if (document) { return service.computeDefinition(textDocumentPosition.textDocument, document.getText(), textDocumentPosition.position); } return null; }); }); connection.onRenameRequest((params) => { return getDocument(params.textDocument.uri).then((document) => { if (document) { let edits = service.computeRename(params.textDocument, document.getText(), params.position, params.newName); return { changes: { [params.textDocument.uri]: edits } }; } return null; }); }); connection.onPrepareRename((params) => { return getDocument(params.textDocument.uri).then((document) => { if (document) { return service.prepareRename(document.getText(), params.position); } return null; }); }); connection.onDocumentSymbol((documentSymbolParams) => { return getDocument(documentSymbolParams.textDocument.uri).then((document) => { if (document) { return service.computeSymbols(documentSymbolParams.textDocument, document.getText()); } return []; }); }); connection.onDocumentFormatting((documentFormattingParams) => { return getDocument(documentFormattingParams.textDocument.uri).then((document) => { if (configurationSupport) { return getFormatterConfiguration(document.uri).then((configuration) => { if (document) { const options = documentFormattingParams.options; options.ignoreMultilineInstructions = configuration !== null && configuration.ignoreMultilineInstructions; return service.format(document.getText(), options); } return []; }); } if (document) { const options = documentFormattingParams.options; options.ignoreMultilineInstructions = formatterConfiguration !== null && formatterConfiguration.ignoreMultilineInstructions; return service.format(document.getText(), options); } return []; }); }); connection.onDocumentRangeFormatting((rangeFormattingParams) => { return getDocument(rangeFormattingParams.textDocument.uri).then((document) => { if (configurationSupport) { return getFormatterConfiguration(document.uri).then((configuration) => { if (document) { const options = rangeFormattingParams.options; options.ignoreMultilineInstructions = configuration !== null && configuration.ignoreMultilineInstructions; return service.formatRange(document.getText(), rangeFormattingParams.range, options); } return []; }); } if (document) { const options = rangeFormattingParams.options; options.ignoreMultilineInstructions = formatterConfiguration !== null && formatterConfiguration.ignoreMultilineInstructions; return service.formatRange(document.getText(), rangeFormattingParams.range, rangeFormattingParams.options); } return []; }); }); connection.onDocumentOnTypeFormatting((onTypeFormattingParams) => { return getDocument(onTypeFormattingParams.textDocument.uri).then((document) => { if (configurationSupport) { return getFormatterConfiguration(document.uri).then((configuration) => { if (document) { const options = onTypeFormattingParams.options; options.ignoreMultilineInstructions = configuration !== null && configuration.ignoreMultilineInstructions; return service.formatOnType(document.getText(), onTypeFormattingParams.position, onTypeFormattingParams.ch, options); } return []; }); } if (document) { const options = onTypeFormattingParams.options; options.ignoreMultilineInstructions = formatterConfiguration !== null && formatterConfiguration.ignoreMultilineInstructions; return service.formatOnType(document.getText(), onTypeFormattingParams.position, onTypeFormattingParams.ch, onTypeFormattingParams.options); } return []; }); }); connection.onDocumentLinks((documentLinkParams) => { return getDocument(documentLinkParams.textDocument.uri).then((document) => { if (document) { return service.computeLinks(document.getText()); } return []; }); }); connection.onDocumentLinkResolve((documentLink) => { return service.resolveLink(documentLink); }); connection.onFoldingRanges((foldingRangeParams) => { return getDocument(foldingRangeParams.textDocument.uri).then((document) => { if (document) { return service.computeFoldingRanges(document.getText()); } return []; }); }); connection.onDidOpenTextDocument((didOpenTextDocumentParams) => { let document = vscode_languageserver_textdocument_1.TextDocument.create(didOpenTextDocumentParams.textDocument.uri, didOpenTextDocumentParams.textDocument.languageId, didOpenTextDocumentParams.textDocument.version, didOpenTextDocumentParams.textDocument.text); documents[didOpenTextDocumentParams.textDocument.uri] = document; validateTextDocument(document); }); connection.languages.semanticTokens.on((semanticTokenParams) => { return getDocument(semanticTokenParams.textDocument.uri).then((document) => { if (document) { return service.computeSemanticTokens(document.getText()); } return { data: [] }; }); }); connection.onDidChangeTextDocument((didChangeTextDocumentParams) => { let document = documents[didChangeTextDocumentParams.textDocument.uri]; let buffer = document.getText(); let content = buffer; let changes = didChangeTextDocumentParams.contentChanges; for (let i = 0; i < changes.length; i++) { const change = changes[i]; if (!change.range && !change.rangeLength) { // no ranges defined, the text is the entire document then buffer = change.text; document = vscode_languageserver_textdocument_1.TextDocument.create(didChangeTextDocumentParams.textDocument.uri, document.languageId, didChangeTextDocumentParams.textDocument.version, buffer); break; } let offset = document.offsetAt(change.range.start); let end = null; if (change.range.end) { end = document.offsetAt(change.range.end); } else { end = offset + change.rangeLength; } buffer = buffer.substring(0, offset) + change.text + buffer.substring(end); document = vscode_languageserver_textdocument_1.TextDocument.create(didChangeTextDocumentParams.textDocument.uri, document.languageId, didChangeTextDocumentParams.textDocument.version, buffer); } documents[didChangeTextDocumentParams.textDocument.uri] = document; if (content !== buffer) { validateTextDocument(document); } }); connection.onDidCloseTextDocument((didCloseTextDocumentParams) => { validatorConfigurations.delete(didCloseTextDocumentParams.textDocument.uri); connection.sendDiagnostics({ uri: didCloseTextDocumentParams.textDocument.uri, diagnostics: [] }); delete documents[didCloseTextDocumentParams.textDocument.uri]; }); // setup complete, start listening for a client connection connection.listen();