dockerfile-language-server-nodejs
Version:
A language server for Dockerfiles powered by NodeJS, TypeScript, and VSCode technologies.
677 lines (676 loc) • 28.9 kB
JavaScript
/* --------------------------------------------------------------------------------------------
* Copyright (c) Remy Suen. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
;
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();