From 23cfd2331f72d578d152bdae059d29b020f0bb2a Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 13 Sep 2021 13:35:58 -0400 Subject: [PATCH] Add the Compose language service (#3206) --- package-lock.json | 55 +++ package.json | 15 +- package.nls.json | 1 + .../dockerComposeCompletionItemProvider.ts | 78 ---- .../dockerComposeHoverProvider.ts | 77 ---- src/dockerCompose/dockerComposeKeyInfo.ts | 343 ------------------ src/dockerCompose/dockerComposeParser.ts | 86 ----- src/dockerHubSearch.ts | 10 - src/extension.ts | 84 +++-- src/parser.ts | 55 --- src/utils/suggestSupportHelper.ts | 80 ---- webpack.config.js | 3 +- 12 files changed, 119 insertions(+), 768 deletions(-) delete mode 100644 src/dockerCompose/dockerComposeCompletionItemProvider.ts delete mode 100644 src/dockerCompose/dockerComposeHoverProvider.ts delete mode 100644 src/dockerCompose/dockerComposeKeyInfo.ts delete mode 100644 src/dockerCompose/dockerComposeParser.ts delete mode 100644 src/parser.ts diff --git a/package-lock.json b/package-lock.json index 08bd136061..220ad4dedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@azure/storage-blob": "^12.4.1", "@docker/sdk": "^1.0.3", "@grpc/grpc-js": "^1.2.12", + "@microsoft/compose-language-service": "^0.0.1-alpha", "dayjs": "^1.10.4", "dockerfile-language-server-nodejs": "^0.6.0", "dockerode": "^3.2.1", @@ -575,6 +576,27 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, + "node_modules/@microsoft/compose-language-service": { + "version": "0.0.1-alpha", + "resolved": "https://registry.npmjs.org/@microsoft/compose-language-service/-/compose-language-service-0.0.1-alpha.tgz", + "integrity": "sha512-MLJL+FOWUYQ/ggze8XTjidaDBSEMvd7Kt6Q6R4QQB16cmFWydqubaO8mftV+HPpQwtACYTjfQh84MUoYhJviPQ==", + "dependencies": { + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "yaml": "^2.0.0-8" + } + }, + "node_modules/@microsoft/compose-language-service/node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6982,6 +7004,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.0.0-8", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-8.tgz", + "integrity": "sha512-QaYgJZMfWD6fKN/EYMk6w1oLWPCr1xj9QaPSZW5qkDb3y8nGCXhy2Ono+AF4F+CSL/vGcqswcAT0BaS//pgD2A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -7514,6 +7544,26 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, + "@microsoft/compose-language-service": { + "version": "0.0.1-alpha", + "resolved": "https://registry.npmjs.org/@microsoft/compose-language-service/-/compose-language-service-0.0.1-alpha.tgz", + "integrity": "sha512-MLJL+FOWUYQ/ggze8XTjidaDBSEMvd7Kt6Q6R4QQB16cmFWydqubaO8mftV+HPpQwtACYTjfQh84MUoYhJviPQ==", + "requires": { + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "yaml": "^2.0.0-8" + }, + "dependencies": { + "vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "requires": { + "vscode-languageserver-protocol": "3.16.0" + } + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12428,6 +12478,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yaml": { + "version": "2.0.0-8", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-8.tgz", + "integrity": "sha512-QaYgJZMfWD6fKN/EYMk6w1oLWPCr1xj9QaPSZW5qkDb3y8nGCXhy2Ono+AF4F+CSL/vGcqswcAT0BaS//pgD2A==" + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 8226840947..b1deaf49de 100644 --- a/package.json +++ b/package.json @@ -2308,15 +2308,11 @@ "default": "docker", "description": "%vscode-docker.config.docker.dockerPath%", "scope": "machine-overridable" - } - } - }, - "configurationDefaults": { - "[dockercompose]": { - "editor.quickSuggestions": { - "other": true, - "comments": false, - "strings": true + }, + "docker.enableDockerComposeLanguageService": { + "type": "boolean", + "default": true, + "description": "%vscode-docker.config.docker.enableDockerComposeLanguageService%" } } }, @@ -3016,6 +3012,7 @@ "@azure/storage-blob": "^12.4.1", "@docker/sdk": "^1.0.3", "@grpc/grpc-js": "^1.2.12", + "@microsoft/compose-language-service": "^0.0.1-alpha", "dayjs": "^1.10.4", "dockerfile-language-server-nodejs": "^0.6.0", "dockerode": "^3.2.1", diff --git a/package.nls.json b/package.nls.json index 555a48b826..2c553155d5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -194,6 +194,7 @@ "vscode-docker.config.docker.scaffolding.templatePath": "The path to use for scaffolding templates.", "vscode-docker.config.docker.showStartPage": "Show the Docker extension Start Page when a new update is released.", "vscode-docker.config.docker.dockerPath": "Absolute path to Docker client executable ('docker' command). If the path contains whitespace, it needs to be quoted appropriately.", + "vscode-docker.config.docker.enableDockerComposeLanguageService": "Whether or not to enable the preview Docker Compose Language Service. Changing requires restart to take effect.", "vscode-docker.config.deprecated": "This setting has been deprecated and will be removed in a future release.", "vscode-docker.commands.compose.down": "Compose Down", "vscode-docker.commands.compose.restart": "Compose Restart", diff --git a/src/dockerCompose/dockerComposeCompletionItemProvider.ts b/src/dockerCompose/dockerComposeCompletionItemProvider.ts deleted file mode 100644 index 059e039847..0000000000 --- a/src/dockerCompose/dockerComposeCompletionItemProvider.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemProvider, Position, TextDocument } from 'vscode'; -import { KeyInfo } from '../extension'; -import helper = require('../utils/suggestSupportHelper'); -import composeVersions from './dockerComposeKeyInfo'; - -export class DockerComposeCompletionItemProvider implements CompletionItemProvider { - - public triggerCharacters: string[] = []; - public excludeTokens: string[] = []; - - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ // Grandfathered in - public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { - const yamlSuggestSupport = new helper.SuggestSupportHelper(); - - // Determine the schema version of the current compose file, - // based on the existence of a top-level "version" property. - const versionMatch = document.getText().match(/^version:\s*(["'])(\d+(\.\d)?)\1/im); - const version = versionMatch ? versionMatch[2] : "1"; - - // Get the line where intellisense was invoked on (e.g. 'image: u'). - const line = document.lineAt(position.line).text; - - if (line.length === 0) { - // empty line - return Promise.resolve(this.suggestKeys('', version)); - } - - const range = document.getWordRangeAtPosition(position); - - // Get the text where intellisense was invoked on (e.g. 'u'). - const word = range && document.getText(range) || ''; - - const textBefore = line.substring(0, position.character); - if (/^\s*[\w_]*$/.test(textBefore)) { - // on the first token - return Promise.resolve(this.suggestKeys(word, version)); - } - - // Matches strings like: 'image: "ubuntu' - const imageTextWithQuoteMatchYaml = textBefore.match(/^\s*image\s*:\s*"([^"]*)$/); - - if (imageTextWithQuoteMatchYaml) { - const imageText = imageTextWithQuoteMatchYaml[1]; - return yamlSuggestSupport.suggestImages(imageText); - } - - // Matches strings like: 'image: ubuntu' - const imageTextWithoutQuoteMatch = textBefore.match(/^\s*image\s*:\s*([\w:/]*)/); - - if (imageTextWithoutQuoteMatch) { - const imageText = imageTextWithoutQuoteMatch[1]; - return yamlSuggestSupport.suggestImages(imageText); - } - - return Promise.resolve([]); - } - - private suggestKeys(word: string, version: string): CompletionItem[] { - // Attempt to grab the keys for the requested schema version, - // otherwise, fall back to showing a composition of all possible keys. - const keys = composeVersions[`v${version}`] || composeVersions.all; - - return Object.keys(keys).map(ruleName => { - const completionItem = new CompletionItem(ruleName); - completionItem.kind = CompletionItemKind.Keyword; - completionItem.insertText = ruleName + ': '; - completionItem.documentation = keys[ruleName]; - return completionItem; - }); - } -} diff --git a/src/dockerCompose/dockerComposeHoverProvider.ts b/src/dockerCompose/dockerComposeHoverProvider.ts deleted file mode 100644 index b423ba14aa..0000000000 --- a/src/dockerCompose/dockerComposeHoverProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { CancellationToken, Hover, HoverProvider, MarkedString, Position, Range, TextDocument } from 'vscode'; -import { KeyInfo } from "../extension"; -import parser = require('../parser'); -import suggestHelper = require('../utils/suggestSupportHelper'); - -export class DockerComposeHoverProvider implements HoverProvider { - // Provide the parser you want to use as well as keyinfo dictionary. - public constructor(public readonly parser: parser.Parser, public readonly keyInfo: KeyInfo) { - } - - public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable { - const line = document.lineAt(position.line); - - if (line.text.length === 0) { - return Promise.resolve(null); - } - - const tokens = this.parser.parseLine(line); - return this.computeInfoForLineWithTokens(line.text, tokens, position); - } - - private computeInfoForLineWithTokens(line: string, tokens: parser.IToken[], position: Position): Promise { - const possibleTokens = this.parser.tokensAtColumn(tokens, position.character); - - return Promise.all(possibleTokens.map(tokenIndex => this.computeInfoForToken(line, tokens, tokenIndex))).then((results) => { - return possibleTokens.map((tokenIndex, arrayIndex) => { - return { - startIndex: tokens[tokenIndex].startIndex, - endIndex: tokens[tokenIndex].endIndex, - result: results[arrayIndex] - }; - }); - }).then((results) => { - const filteredResults = results.filter(r => !!r.result); - if (filteredResults.length === 0) { - return; - } - - const range = new Range(position.line, filteredResults[0].startIndex, position.line, filteredResults[0].endIndex); - - const hover = new Hover(filteredResults[0].result, range); - - return hover; - - }); - } - - private computeInfoForToken(line: string, tokens: parser.IToken[], tokenIndex: number): Promise { - // ------------- - // Detect hovering on a key - if (tokens[tokenIndex].type === parser.TokenType.Key) { - const keyName = this.parser.keyNameFromKeyToken(this.parser.tokenValue(line, tokens[tokenIndex])).trim(); - const r = this.keyInfo[keyName]; - if (r) { - return Promise.resolve([r]); - } - } - - // ------------- - // Detect <> - // Detect <> - const helper = new suggestHelper.SuggestSupportHelper(); - const r2 = helper.getImageNameHover(line, this.parser, tokens, tokenIndex); - if (r2) { - return r2; - } - - return; - } -} diff --git a/src/dockerCompose/dockerComposeKeyInfo.ts b/src/dockerCompose/dockerComposeKeyInfo.ts deleted file mode 100644 index 7a0640abc9..0000000000 --- a/src/dockerCompose/dockerComposeKeyInfo.ts +++ /dev/null @@ -1,343 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ComposeVersionKeys, KeyInfo } from "../extension"; -import { localize } from "../localize"; - -// Define the keys that are shared between all compose file versions, -// regardless of the major/minor version (e.g. v1-v2.1+). -// https://docs.docker.com/compose/yml/ - -/* eslint-disable @typescript-eslint/naming-convention */ - -const DOCKER_COMPOSE_SHARED_KEY_INFO: KeyInfo = { - 'build': ( - localize('vscode-docker.dockerComposeKey.build', 'Path to a directory containing a Dockerfile. When the value supplied is a relative path, it is interpreted as relative to the location of the yml file itself. This directory is also the build context that is sent to the Docker daemon.\n\nCompose will build and tag it with a generated name, and use that image thereafter.') - ), - 'cap_add': ( - localize('vscode-docker.dockerComposeKey.cap_add', 'Add or drop container capabilities. See `man 7 capabilities` for a full list.') - ), - 'cap_drop': ( - localize('vscode-docker.dockerComposeKey.cap_drop', 'Add or drop container capabilities. See `man 7 capabilities` for a full list.') - ), - 'cgroup_parent': ( - localize('vscode-docker.dockerComposeKey.cgroup_parent', 'Specify an optional parent cgroup for the container.') - ), - 'command': ( - localize('vscode-docker.dockerComposeKey.command', 'Override the default command.') - ), - 'container_name': ( - localize('vscode-docker.dockerComposeKey.container_name', 'Specify custom container name, rather than a generated default name.') - ), - 'cpu_shares': ( - localize('vscode-docker.dockerComposeKey.cpu_shares', 'CPU shares (relative weight).') - ), - 'cpu_quota': ( - localize('vscode-docker.dockerComposeKey.cpu_quota', 'Limit CPU CFS (Completely Fair Scheduler) quota.') - ), - 'cpuset': ( - localize('vscode-docker.dockerComposeKey.cpuset', 'CPUs in which to allow execution.') - ), - 'devices': ( - localize('vscode-docker.dockerComposeKey.devices', 'List of device mappings. Uses the same format as the `--device` docker client create option.') - ), - 'dns': ( - localize('vscode-docker.dockerComposeKey.dns', 'Custom DNS servers. Can be a single value or a list.') - ), - 'dns_search': ( - localize('vscode-docker.dockerComposeKey.dns_search', 'Custom DNS search domains. Can be a single value or a list.') - ), - 'dockerfile': ( - localize('vscode-docker.dockerComposeKey.dockerfile', 'Alternate Dockerfile. Compose will use an alternate file to build with. Using `dockerfile` together with `image` is not allowed. Attempting to do so results in an error.') - ), - 'domainname': ( - localize('vscode-docker.dockerComposeKey.domainname', 'Container domain name.') - ), - 'entrypoint': ( - localize('vscode-docker.dockerComposeKey.entrypoint', 'Overwrite the default ENTRYPOINT of the image.') - ), - 'env_file': ( - localize('vscode-docker.dockerComposeKey.env_file', 'Add environment variables from a file. Can be a single value or a list.\n\nIf you have specified a Compose file with `docker-compose -f FILE`, paths in `env_file` are relative to the directory that file is in.\n\nEnvironment variables specified in `environment` override these values.') - ), - 'environment': ( - localize('vscode-docker.dockerComposeKey.environment', 'Add environment variables. You can use either an array or a dictionary.\n\nEnvironment variables with only a key are resolved to their values on the machine Compose is running on, which can be helpful for secret or host-specific values.') - ), - 'expose': ( - localize('vscode-docker.dockerComposeKey.expose', 'Expose ports without publishing them to the host machine - they\'ll only be accessible to linked services.\nOnly the internal port can be specified.') - ), - 'extends': ( - localize('vscode-docker.dockerComposeKey.extends', 'Extend another service, in the current file or another, optionally overriding configuration.\nYou can use `extends` on any service together with other configuration keys. The `extends` value must be a dictionary defined with a required `service` and an optional `file` key.') - ), - 'external_links': ( - localize('vscode-docker.dockerComposeKey.external_links', 'Link to containers started outside this `docker-compose.yml` or even outside of Compose, especially for containers that provide shared or common services. `external_links` follow semantics similar to `links` when specifying both the container name and the link alias (`CONTAINER:ALIAS`).') - ), - 'extra_hosts': ( - localize('vscode-docker.dockerComposeKey.extra_hosts', 'Add hostname mappings. Use the same values as the docker client `--add-host` parameter.') - ), - 'hostname': ( - localize('vscode-docker.dockerComposeKey.hostname', 'Container host name.') - ), - 'image': ( - localize('vscode-docker.dockerComposeKey.image', 'Tag or partial image ID. Can be local or remote - Compose will attempt to pull if it doesn\'t exist locally.') - ), - 'ipc': ( - localize('vscode-docker.dockerComposeKey.ipc', 'IPC namespace to use.') - ), - 'labels': ( - localize('vscode-docker.dockerComposeKey.labels', 'Add metadata to containers using Docker labels. You can either use an array or a dictionary.\nIt\'s recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software.') - ), - 'links': ( - localize('vscode-docker.dockerComposeKey.links', 'Link to containers in another service. Either specify both the service name and the link alias (`CONTAINER:ALIAS`), or just the service name (which will also be used for the alias).') - ), - 'mac_address': ( - localize('vscode-docker.dockerComposeKey.mac_address', 'Container MAC address (e.g. 92:d0:c6:0a:29:33).') - ), - 'mem_limit': ( - localize('vscode-docker.dockerComposeKey.mem_limit', 'Memory limit.') - ), - 'memswap_limit': ( - localize('vscode-docker.dockerComposeKey.memswap_limit', 'Swap limit equal to memory plus swap: \'-1\' to enable unlimited swap.') - ), - 'mem_swappiness': ( - localize('vscode-docker.dockerComposeKey.mem_swappiness', 'Tune container memory swappiness (0 to 100) (default -1).') - ), - 'pid': ( - localize('vscode-docker.dockerComposeKey.pid', 'Sets the PID mode to the host PID mode. This turns on sharing between container and the host operating system the PID address space. Containers launched with this flag will be able to access and manipulate other containers in the bare-metal machine\'s namespace and vise-versa.') - ), - 'ports': ( - localize('vscode-docker.dockerComposeKey.ports', 'Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).\n\n**Note**: When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings.') - ), - 'privileged': ( - localize('vscode-docker.dockerComposeKey.privileged', 'Give extended privileges to this container.') - ), - 'read_only': ( - localize('vscode-docker.dockerComposeKey.read_only', 'Mount the container\'s root filesystem as read only.') - ), - 'restart': ( - localize('vscode-docker.dockerComposeKey.restart', 'Restart policy to apply when a container exits (default "no").') - ), - 'security_opt': ( - localize('vscode-docker.dockerComposeKey.security_opt', 'Override the default labeling scheme for each container.') - ), - 'shm_size': ( - localize('vscode-docker.dockerComposeKey.shm_size', 'Size of /dev/shm, default value is 64MB.') - ), - 'stdin_open': ( - localize('vscode-docker.dockerComposeKey.stdin_open', 'Keep STDIN open even if not attached.') - ), - 'stop_signal': ( - localize('vscode-docker.dockerComposeKey.stop_signal', 'Signal to stop a container, SIGTERM by default.') - ), - 'tty': ( - localize('vscode-docker.dockerComposeKey.tty', 'Allocate a pseudo-TTY.') - ), - 'ulimits': ( - localize('vscode-docker.dockerComposeKey.ulimits', 'Override the default ulimits for a container. You can either specify a single limit as an integer or soft/hard limits as a mapping.') - ), - 'user': ( - localize('vscode-docker.dockerComposeKey.user', 'Username or UID (format: [:]).') - ), - 'version': ( - localize('vscode-docker.dockerComposeKey.version', 'Specify the compose format that this file conforms to.') - ), - 'volumes': ( - localize('vscode-docker.dockerComposeKey.volumes', 'Mount paths as volumes, optionally specifying a path on the host machine (`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).') - ), - 'volume_driver': ( - localize('vscode-docker.dockerComposeKey.volume_driver', 'If you use a volume name (instead of a volume path), you may also specify a `volume_driver`.') - ), - 'volumes_from': ( - localize('vscode-docker.dockerComposeKey.volumes_from', 'Mount all of the volumes from another service or container.') - ), - 'working_dir': ( - localize('vscode-docker.dockerComposeKey.working_dir', 'Working directory inside the container.') - ) -}; - -// Define the keys which are unique to the v1 format, and were deprecated in v2+. -// https://github.com/docker/compose/blob/master/compose/config/config_schema_v1.json -const DOCKER_COMPOSE_V1_KEY_INFO: KeyInfo = { - 'log_driver': ( - localize('vscode-docker.dockerComposeKey1.log_driver', 'Specify a logging driver for the service\'s containers, as with the `--log-driver` option for docker run. The default value is json-file.') - ), - 'log_opt': ( - localize('vscode-docker.dockerComposeKey1.log_opt', 'Specify logging options with `log_opt` for the logging driver, as with the `--log-opt` option for docker run.') - ), - 'net': ( - localize('vscode-docker.dockerComposeKey1.net', 'Networking mode. Use the same values as the docker client `--net` parameter.') - ) -}; - -// Define the keys which are shared with all v2+ compose file versions, but weren't defined in v1. -// https://github.com/docker/compose/blob/master/compose/config/config_schema_v2.0.json -const DOCKER_COMPOSE_V2_KEY_INFO: KeyInfo = { - // Added top-level properties - 'services': ( - localize('vscode-docker.dockerComposeKey2.services', 'Specify the set of services that your app is composed of.') - ), - - // TODO: There is now a top-level and service-level volumes/networks setting which conflict. - // This will be resolved when we add completion that understands file position context. - 'networks': ( - localize('vscode-docker.dockerComposeKey2.networks', 'Specifies the networks to be created as part of your app. This is analogous to running `docker network create`.') - ), - 'volumes': ( - localize('vscode-docker.dockerComposeKey2.volumes', 'Specifies the volumes to be created as part of your app. This is analogous to running `docker volume create`.') - ), - - // Added service-level properties - 'depends_on': ( - localize('vscode-docker.dockerComposeKey2.depends_on', 'Specifies the names of services that this service depends on.') - ), - 'logging': ( - localize('vscode-docker.dockerComposeKey2.logging', 'Logging configuration for the service.') - ), - 'network_mode': ( - localize('vscode-docker.dockerComposeKey2.network_mode', 'Networking mode. Use the same values as the docker client `--net` parameter.') - ), - 'tmpfs': ( - localize('vscode-docker.dockerComposeKey2.tmpfs', 'Mount a temporary file system inside the container. Can be a single value or a list.') - ), - - // Modified service-level properties - 'build': ( - localize('vscode-docker.dockerComposeKey2.build', 'Configuration options that are applied at build time. Can be specified either as a string containing a path to the build context, or an object with the path specified under `context` and optionally `dockerfile` and `args`.') - ), - - // Added service/logging-level properties - // TODO: The "driver" property could be a logging driver, a volume driver, - // a network driver, or a network IPAM driver, so we should account for - // that when we add context-based completion. - 'driver': ( - localize('vscode-docker.dockerComposeKey2.driver', 'Specifies the logging driver to use for the service’s container.') - ), - 'options': ( - localize('vscode-docker.dockerComposeKey2.options', 'Options to pass to the specified logging driver, provided as key-value pairs.') - ), - - // Added service/build-level properties - 'args': ( - localize('vscode-docker.dockerComposeKey2.args', 'Add build arguments, which are environment variables accessible only during the build process.') - ), - 'context': ( - localize('vscode-docker.dockerComposeKey2.context', 'Either a path to a directory containing a Dockerfile, or a url to a git repository. This directory will be used as the build context that is sent to the Docker daemon.') - ), - - // Added service/network-level properties - 'aliases': ( - localize('vscode-docker.dockerComposeKey2.aliases', 'Alternative hostnames for this service on the network. Other containers on the same network can use either the service name or this alias to connect to one of the service’s containers.') - ), - 'ipv4_address': ( - localize('vscode-docker.dockerComposeKey2.ipv4_address', 'Specify a static IPv4 address for containers for this service when joining the network.') - ), - 'ipv6_address': ( - localize('vscode-docker.dockerComposeKey2.ipv6_address', 'Specify a static IPv6 address for containers for this service when joining the network.') - ), - - // Network-level properties - 'driver_opts': ( - localize('vscode-docker.dockerComposeKey2.driver_opts', 'Specify a list of options as key-value pairs to pass to the driver. Those options are driver-dependent - consult the driver’s documentation for more information.') - ), - 'external': ( - localize('vscode-docker.dockerComposeKey2.external', 'If set to true, specifies that this network has been created outside of Compose. `docker-compose up` will not attempt to create it, and will raise an error if it doesn’t exist.') - ), - 'ipam': ( - localize('vscode-docker.dockerComposeKey2.ipam', 'Specify custom IPAM config') - ), - - // Network/external-level properties - // TODO: This would also apply to an external volume, - // so we should account for that when we add context-based completion. - 'name': ( - localize('vscode-docker.dockerComposeKey2.name', 'Specifies the name of the externally defined network.') - ), - - // Network/ipam-level properties - 'config': ( - localize('vscode-docker.dockerComposeKey2.config', 'A list with zero or more config blocks.') - ), - - // Network/ipam/config-level properties - 'aux_addresses': ( - localize('vscode-docker.dockerComposeKey2.aux_addresses', 'Auxiliary IPv4 or IPv6 addresses used by Network driver, as a mapping from hostname to IP.') - ), - 'gateway': ( - localize('vscode-docker.dockerComposeKey2.gateway', 'IPv4 or IPv6 gateway for the master subnet.') - ), - 'ip_range': ( - localize('vscode-docker.dockerComposeKey2.ip_range', 'Range of IPs from which to allocate container IPs.') - ), - 'subnet': ( - localize('vscode-docker.dockerComposeKey2.subnet', 'Subnet in CIDR format that represents a network segment.') - ), - - // Volume-level properties - // TODO: Top-level volumes support specifying the "driver", - // "driver_opt", and "external" properties, but these are already - // defined above. We can specialize these by adding context-based completion. -}; - -// Define the keys which were introduced in the v2.1 format. -// https://github.com/docker/compose/blob/master/compose/config/config_schema_v2.1.json -const DOCKER_COMPOSE_V2_1_KEY_INFO: KeyInfo = { - // Added service-level properties - 'group_add': ( - localize('vscode-docker.dockerComposeKey21.group_add', 'Specifies additional groups to join') - ), - 'isolation': ( - localize('vscode-docker.dockerComposeKey21.isolation', 'Container isolation technology') - ), - 'oom_score_adj': ( - localize('vscode-docker.dockerComposeKey21.oom_score_adj', 'Tune host\'s OOM preferences (-1000 to 1000)') - ), - - // Added service/network-level properties - 'link_local_ips': ( - localize('vscode-docker.dockerComposeKey21.link_local_ips', 'List of IPv4/IPv6 link-local addresses for the container') - ), - - // Added network-level properties - 'internal': ( - localize('vscode-docker.dockerComposeKey21.internal', 'Restrict external access to the network') - ), - 'enable_ipv6': ( - localize('vscode-docker.dockerComposeKey21.enable_ipv6', 'Enable IPv6 networking') - ) - - // Note that in v2.1, networks and volumes can now accept a "labels", - // property, however, this label is already defined for services - // in the v2.0 format, so we don't need to re-define it. -}; - -// Define the keys which were introduced in the v2.2 format. -// https://github.com/docker/compose/blob/master/compose/config/config_schema_v2.2.json -const DOCKER_COMPOSE_V2_2_KEY_INFO: KeyInfo = { - // Added service-level properties - 'cpu_count': ( - localize('vscode-docker.dockerComposeKey22.cpu_count', 'Number of usable CPUs (Windows only)') - ), - 'cpu_percent': ( - localize('vscode-docker.dockerComposeKey22.cpu_percent', 'Usable percentage of the available CPUs (Windows only)') - ), - 'cpus': ( - localize('vscode-docker.dockerComposeKey22.cpus', 'CPU quota in number of CPUs') - ) -}; - -/* eslint-enable @typescript-eslint/naming-convention */ - -// Helper function that merges the specified version-specific keys with the shared -// keys, in order to create a complete schema for a specic version. -function mergeWithSharedKeys(...versions: KeyInfo[]): KeyInfo { - return Object.assign({}, DOCKER_COMPOSE_SHARED_KEY_INFO, ...versions); -} - -export default { - v1: mergeWithSharedKeys(DOCKER_COMPOSE_V1_KEY_INFO), - v2: mergeWithSharedKeys(DOCKER_COMPOSE_V2_KEY_INFO), - "v2.1": mergeWithSharedKeys(DOCKER_COMPOSE_V2_KEY_INFO, DOCKER_COMPOSE_V2_1_KEY_INFO), - "v2.2": mergeWithSharedKeys(DOCKER_COMPOSE_V2_KEY_INFO, DOCKER_COMPOSE_V2_1_KEY_INFO, DOCKER_COMPOSE_V2_2_KEY_INFO), - all: mergeWithSharedKeys(DOCKER_COMPOSE_V1_KEY_INFO, DOCKER_COMPOSE_V2_KEY_INFO, DOCKER_COMPOSE_V2_1_KEY_INFO, DOCKER_COMPOSE_V2_2_KEY_INFO) -}; diff --git a/src/dockerCompose/dockerComposeParser.ts b/src/dockerCompose/dockerComposeParser.ts deleted file mode 100644 index 08b7889b27..0000000000 --- a/src/dockerCompose/dockerComposeParser.ts +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import vscode = require('vscode'); -import { IToken, Parser, TokenType } from '../parser'; - -export class DockerComposeParser extends Parser { - public constructor() { - const parseRegex = /:+$/g; - super(parseRegex); - } - - public parseLine(textLine: vscode.TextLine): IToken[] { - const r: IToken[] = []; - let lastTokenEndIndex = 0; - let lastPushedToken: IToken | undefined; - - const emit = (end: number, type: TokenType) => { - if (end <= lastTokenEndIndex) { - return; - } - - if (lastPushedToken && lastPushedToken.type === type) { - // merge with last pushed token - lastPushedToken.endIndex = end; - lastTokenEndIndex = end; - return; - } - - lastPushedToken = { - startIndex: lastTokenEndIndex, - endIndex: end, - type: type - }; - - r.push(lastPushedToken); - lastTokenEndIndex = end; - }; - - let inString = false; - const idx = textLine.firstNonWhitespaceCharacterIndex; - const line = textLine.text; - - for (let i = idx, len = line.length; i < len; i++) { - const ch = line.charAt(i); - - if (inString) { - if (ch === '"' && line.charAt(i - 1) !== '\\') { - inString = false; - emit(i + 1, TokenType.String); - } - - continue; - } - - if (ch === '"') { - emit(i, TokenType.Text); - inString = true; - continue; - } - - if (ch === '#') { - // Comment the rest of the line - emit(i, TokenType.Text); - emit(line.length, TokenType.Comment); - break; - } - - if (ch === ':') { - emit(i + 1, TokenType.Key); - } - - if (ch === ' ' || ch === '\t') { - emit(i, TokenType.Text); - emit(i + 1, TokenType.Whitespace); - } - } - - emit(line.length, TokenType.Text); - return r; - } -} diff --git a/src/dockerHubSearch.ts b/src/dockerHubSearch.ts index 6678008ef4..561670f4d5 100644 --- a/src/dockerHubSearch.ts +++ b/src/dockerHubSearch.ts @@ -25,16 +25,6 @@ export function tagsForImage(image: IHubSearchResponseResult): string { return ''; } -/* eslint-disable-next-line @typescript-eslint/promise-function-async */ // Grandfathered in -export function searchImageInRegistryHub(imageName: string, cache: boolean): Promise { - return invokeHubSearch(imageName, 1, cache).then((data) => { - if ((data.results).length === 0) { - return undefined; - } - return data.results[0]; - }); -} - /* eslint-disable @typescript-eslint/naming-convention */ const popular = [ { "is_automated": false, "name": "redis", "is_trusted": false, "is_official": true, "star_count": 1300, "description": localize('vscode-docker.dockerHubSearch.redis', 'Redis is an open source key-value store that functions as a data structure server.') }, diff --git a/src/extension.ts b/src/extension.ts index ff114ba251..018d0f67b3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,7 @@ import * as fse from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { callWithTelemetryAndErrorHandling, createAzExtOutputChannel, createExperimentationService, IActionContext, registerErrorHandler, registerReportIssueCommand, registerUIExtensionVariables } from 'vscode-azureextensionui'; +import { callWithTelemetryAndErrorHandling, createAzExtOutputChannel, createExperimentationService, IActionContext, registerErrorHandler, registerReportIssueCommand, registerUIExtensionVariables, UserCancelledError } from 'vscode-azureextensionui'; import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/node'; import * as tas from 'vscode-tas-client'; import { registerCommands } from './commands/registerCommands'; @@ -16,10 +16,6 @@ import { extensionVersion } from './constants'; import { registerDebugProvider } from './debugging/DebugHelper'; import { DockerContextManager } from './docker/ContextManager'; import { ContainerFilesProvider } from './docker/files/ContainerFilesProvider'; -import { DockerComposeCompletionItemProvider } from './dockerCompose/dockerComposeCompletionItemProvider'; -import { DockerComposeHoverProvider } from './dockerCompose/dockerComposeHoverProvider'; -import composeVersionKeys from './dockerCompose/dockerComposeKeyInfo'; -import { DockerComposeParser } from './dockerCompose/dockerComposeParser'; import { DockerfileCompletionItemProvider } from './dockerfileCompletionItemProvider'; import { ext } from './extensionVariables'; import { registerTaskProviders } from './tasks/TaskHelper'; @@ -95,25 +91,6 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats: ) ); - const COMPOSE_MODE_ID: vscode.DocumentFilter = { - language: 'dockercompose', - scheme: 'file', - }; - const composeHoverProvider = new DockerComposeHoverProvider( - new DockerComposeParser(), - composeVersionKeys.all - ); - ctx.subscriptions.push( - vscode.languages.registerHoverProvider(COMPOSE_MODE_ID, composeHoverProvider) - ); - ctx.subscriptions.push( - vscode.languages.registerCompletionItemProvider( - COMPOSE_MODE_ID, - new DockerComposeCompletionItemProvider(), - "." - ) - ); - ctx.subscriptions.push(ext.dockerContextManager = new DockerContextManager()); // At initialization we need to force a refresh since the filesystem watcher would have no reason to trigger // No need to wait thanks to ContextLoadingClient @@ -136,7 +113,8 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats: registerDebugProvider(ctx); registerTaskProviders(ctx); - activateLanguageClient(ctx); + activateDockerfileLanguageClient(ctx); + activateComposeLanguageClient(ctx); registerListeners(); }); @@ -236,11 +214,11 @@ namespace Configuration { } /* eslint-enable @typescript-eslint/no-namespace, no-inner-declarations */ -function activateLanguageClient(ctx: vscode.ExtensionContext): void { +function activateDockerfileLanguageClient(ctx: vscode.ExtensionContext): void { // Don't wait void callWithTelemetryAndErrorHandling('docker.languageclient.activate', async (context: IActionContext) => { context.telemetry.properties.isActivationEvent = 'true'; - const serverModule = ext.context.asAbsolutePath( + const serverModule = ctx.asAbsolutePath( path.join( "dist", "dockerfile-language-server-nodejs", @@ -285,8 +263,7 @@ function activateLanguageClient(ctx: vscode.ExtensionContext): void { clientOptions ); client.registerProposedFeatures(); - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - client.onReady().then(() => { + void client.onReady().then(() => { // attach the VS Code settings listener Configuration.initialize(ctx); }); @@ -294,3 +271,52 @@ function activateLanguageClient(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push(client.start()); }); } + +function activateComposeLanguageClient(ctx: vscode.ExtensionContext): void { + // Don't wait + void callWithTelemetryAndErrorHandling('docker.composelanguageclient.activate', async (context: IActionContext) => { + context.telemetry.properties.isActivationEvent = 'true'; + + const config = vscode.workspace.getConfiguration('docker'); + if (!config.get('enableDockerComposeLanguageService', true)) { + throw new UserCancelledError('languageServiceDisabled'); + } + + const serverModule = ctx.asAbsolutePath( + path.join( + "dist", + "compose-language-service", + "lib", + "server.js" + ) + ); + + const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; + + const serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + args: ["--node-ipc"] + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions + } + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ language: 'dockercompose' }] + }; + + client = new LanguageClient( + "compose-language-service", + "Docker Compose Language Server", + serverOptions, + clientOptions + ); + client.registerProposedFeatures(); + ctx.subscriptions.push(client.start()); + }); +} diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index 4e32805f62..0000000000 --- a/src/parser.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TextLine } from 'vscode'; - -export abstract class Parser { - public constructor(public readonly tokenParseRegex: RegExp) { - } - - public keyNameFromKeyToken(keyToken: string): string { - return keyToken.replace(this.tokenParseRegex, ''); - } - - public tokenValue(line: string, token: IToken): string { - return line.substring(token.startIndex, token.endIndex); - } - - public tokensAtColumn(tokens: IToken[], charIndex: number): number[] { - for (let i = 0, len = tokens.length; i < len; i++) { - const token = tokens[i]; - - if (token.endIndex < charIndex) { - continue; - } - - if (token.endIndex === charIndex && i + 1 < len) { - return [i, i + 1]; - } - return [i]; - } - - // should not happen: no token found? => return the last one - return [tokens.length - 1]; - } - - public abstract parseLine(textLine: TextLine): IToken[]; -} - -export enum TokenType { - Whitespace, - Text, - String, - Comment, - Key -} - -export interface IToken { - startIndex: number; - endIndex: number; - type: TokenType; -} diff --git a/src/utils/suggestSupportHelper.ts b/src/utils/suggestSupportHelper.ts index 2e31a96568..1c7ca51628 100644 --- a/src/utils/suggestSupportHelper.ts +++ b/src/utils/suggestSupportHelper.ts @@ -6,9 +6,7 @@ 'use strict'; import vscode = require('vscode'); -import { FROM_DIRECTIVE_PATTERN } from '../constants'; import hub = require('../dockerHubSearch'); -import parser = require('../parser'); export class SuggestSupportHelper { /* eslint-disable-next-line @typescript-eslint/promise-function-async */ // Grandfathered in @@ -30,82 +28,4 @@ export class SuggestSupportHelper { }); }); } - - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ // Grandfathered in - public searchImageInRegistryHub(imageName: string): Promise { - return hub.searchImageInRegistryHub(imageName, true).then((result) => { - if (result) { - const r: vscode.MarkedString[] = []; - const tags = hub.tagsForImage(result); - - // Name, tags and stars. - let nameString = ''; - if (tags.length > 0) { - nameString = '**' + result.name + ' ' + tags + '** '; - } else { - nameString = '**' + result.name + '**'; - } - - if (result.star_count) { - const plural = (result.star_count > 1); - nameString += '**' + String(result.star_count) + (plural ? ' stars' : ' star') + '**'; - } - - r.push(nameString); - - // Description - r.push(result.description); - - return r; - } - }); - } - - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ // Grandfathered in - public getImageNameHover(line: string, prsr: parser.Parser, tokens: parser.IToken[], tokenIndex: number): Promise { - // ------------- - // Detect <> - // Detect <> - const originalValue = prsr.tokenValue(line, tokens[tokenIndex]); - - let keyToken: string = null; - tokenIndex--; - while (tokenIndex >= 0) { - const type = tokens[tokenIndex].type; - if (type === parser.TokenType.String || type === parser.TokenType.Text) { - return; - } - if (type === parser.TokenType.Key) { - keyToken = prsr.tokenValue(line, tokens[tokenIndex]); - break; - } - tokenIndex--; - } - - if (!keyToken) { - return; - } - const keyName = prsr.keyNameFromKeyToken(keyToken); - if (keyName === 'image' || keyName === 'FROM') { - let imageName: string; - - if (keyName === 'FROM') { - imageName = line.match(FROM_DIRECTIVE_PATTERN)[1]; - } else { - imageName = originalValue.replace(/^"/, '').replace(/"$/, ''); - } - - return this.searchImageInRegistryHub(imageName).then((results) => { - if (results[0] && results[1]) { - return ['**DockerHub:**', results[0], '**DockerRuntime**', results[1]]; - } - - if (results[0]) { - return [results[0]]; - } - - return [results[1]]; - }); - } - } } diff --git a/webpack.config.js b/webpack.config.js index e5353eed91..7e93bd10f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,7 +18,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const debugWebpack = !!process.env.DEBUG_WEBPACK; -/** @type {import('webpack').Configuration}*/ // Here's where we can get typing help even though it's JS +/** @type {import('webpack').Configuration} */ // Here's where we can get typing help even though it's JS const config = { target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') @@ -27,6 +27,7 @@ const config = { entry: { './extension.bundle': './src/extension.ts', './dockerfile-language-server-nodejs/lib/server': './node_modules/dockerfile-language-server-nodejs/lib/server.js', + './compose-language-service/lib/server': './node_modules/@microsoft/compose-language-service/lib/server.js', }, // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/