diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 1ca151e..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- - -FROM debian:latest -# FROM debian:8 - -# Avoid warnings by switching to noninteractive -ENV DEBIAN_FRONTEND=noninteractive - -# Configure apt and install packages -RUN apt-get update \ - && apt-get -y install --no-install-recommends apt-utils 2>&1 \ - # - # Install the Azure CLI - && apt-get install -y apt-transport-https curl gnupg2 lsb-release \ - && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list \ - && curl -sL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 2>/dev/null \ - && apt-get update - -RUN apt-get install -y azure-cli -# RUN apt-get install -y azure-cli=2.0.67-1~jessie -# RUN apt-get install -y azure-cli=2.0.64-1~jessie -# RUN apt-get install -y azure-cli=2.0.63-1~jessie -# RUN apt-get install -y azure-cli=2.0.26-1~jessie - -# Switch back to dialog for any ad-hoc use of apt-get -ENV DEBIAN_FRONTEND=dialog diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 4a62381..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,5 +0,0 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. -{ - "name": "Azure CLI", - "dockerFile": "Dockerfile", -} \ No newline at end of file diff --git a/.devcontainer/docker-compose.test.yml b/.devcontainer/docker-compose.test.yml deleted file mode 100644 index 50626d7..0000000 --- a/.devcontainer/docker-compose.test.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: '3.4' -services: - vscode-azurecli-test: - build: - context: . - dockerfile: Dockerfile - working_dir: "/workspace" - volumes: - - "${PWD}:/workspace" - command: "./service/run_tests" - -# docker-compose -f .devcontainer/docker-compose.test.yml build --no-cache --pull -# docker-compose -f .devcontainer/docker-compose.test.yml run vscode-azurecli-test diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8160068..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -{ - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], - "preLaunchTask": "npm: watch" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 67f8e75..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - "python.pythonPath": "~/lib/azure-cli/bin/python" -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 604e38f..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,20 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 291192e..0000000 --- a/package-lock.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "name": "azurecli", - "version": "0.6.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "azurecli", - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "elegant-spinner": "2.0.0", - "jmespath": "0.15.0", - "semver": "7.5.2" - }, - "devDependencies": { - "@types/jmespath": "0.15.0", - "@types/node": "10.14.1", - "@types/semver": "5.5.0", - "@types/vscode": "^1.75.0", - "typescript": "^5.3.3" - }, - "engines": { - "vscode": "^1.75.0" - } - }, - "node_modules/@types/jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-SgFikJaoYjHIkaDi3szBX1PJKR0=", - "dev": true - }, - "node_modules/@types/node": { - "version": "10.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.1.tgz", - "integrity": "sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", - "dev": true - }, - "node_modules/@types/vscode": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.85.0.tgz", - "integrity": "sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==", - "dev": true - }, - "node_modules/elegant-spinner": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-2.0.0.tgz", - "integrity": "sha512-5YRYHhvhYzV/FC4AiMdeSIg3jAYGq9xFvbhZMpPlJoBsfYgrw2DSCYeXfat6tYBu45PWiyRr3+flaCPPmviPaA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/package.json b/package.json index 191899e..d6ddef5 100644 --- a/package.json +++ b/package.json @@ -1,152 +1,131 @@ { - "name": "azurecli", - "displayName": "Azure CLI Tools", - "description": "Tools for developing and running commands of the Azure CLI.", - "version": "0.6.0", - "icon": "images/azure_icon.png", - "publisher": "ms-vscode", - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-azurecli.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/Microsoft/vscode-azurecli/issues" - }, - "engines": { - "vscode": "^1.75.0" - }, - "categories": [ - "Programming Languages", - "Snippets", - "Azure" - ], - "main": "./out/src/extension", - "l10n": "./l10n", - "contributes": { - "languages": [ - { - "id": "azcli", - "aliases": [ - "Azure CLI Scrapbook", - "azcli" - ], - "extensions": [ - ".azcli" - ], - "configuration": "./language-configuration.json" - } - ], - "grammars": [ - { - "language": "azcli", - "scopeName": "source.azcli", - "path": "./syntaxes/azcli.tmLanguage.json" - } - ], - "commands": [ - { - "category": "Azure CLI", - "command": "ms-azurecli.runLineInTerminal", - "title": "%runLineInTerminal.title%" - }, - { - "category": "Azure CLI", - "command": "ms-azurecli.runLineInEditor", - "title": "%runLineInEditor.title%" - }, - { - "category": "Azure CLI", - "command": "ms-azurecli.toggleLiveQuery", - "title": "%toggleLiveQuery.title%" - }, - { - "category": "Azure CLI", - "command": "ms-azurecli.installAzureCLI", - "title": "%installAzureCLI.title%" - } - ], - "keybindings": [ - { - "command": "ms-azurecli.runLineInTerminal", - "key": "ctrl+'", - "mac": "cmd+'", - "when": "editorTextFocus && editorLangId == 'azcli'" - }, - { - "command": "ms-azurecli.runLineInEditor", - "key": "ctrl+shift+'", - "mac": "cmd+shift+'", - "when": "editorTextFocus && editorLangId == 'azcli'" - }, - { - "command": "ms-azurecli.toggleLiveQuery", - "key": "ctrl+;", - "mac": "cmd+;", - "when": "editorTextFocus && editorLangId == 'azcli'" - } - ], - "configuration": { - "type": "object", - "title": "%configuration.title%", - "properties": { - "azureCLI.showResultInNewEditor": { - "type": "boolean", - "default": false, - "scope": "resource", - "description": "%azureCLI.showResultInNewEditor.description%" - }, - "azureCLI.lineContinuationCharacter": { - "type": "string", - "default": "", - "scope": "resource", - "description": "%azureCLI.lineContinuationCharacter.description%" - } - } - }, - "menus": { - "editor/context": [ - { - "command": "ms-azurecli.runLineInTerminal", - "group": "2_run", - "when": "editorLangId == 'azcli'" - }, - { - "command": "ms-azurecli.runLineInEditor", - "group": "2_run", - "when": "editorLangId == 'azcli'" - } - ], - "commandPalette": [ - { - "command": "ms-azurecli.runLineInTerminal", - "when": "editorLangId == 'azcli'" - }, - { - "command": "ms-azurecli.runLineInEditor", - "when": "editorLangId == 'azcli'" - }, - { - "command": "ms-azurecli.toggleLiveQuery", - "when": "editorLangId == 'azcli'" - } - ] - } - }, - "scripts": { - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./" - }, - "devDependencies": { - "@types/jmespath": "0.15.0", - "@types/node": "10.14.1", - "@types/semver": "5.5.0", - "@types/vscode": "^1.75.0", - "typescript": "^5.3.3" - }, - "dependencies": { - "jmespath": "0.15.0", - "semver": "7.5.2", - "elegant-spinner": "2.0.0" - } + "name": "azurecli", + "displayName": "Azure CLI Tools", + "description": "Tools for developing and running commands of the Azure CLI.", + "main": "./out/src/extension", + "scripts": { + "compile": "tsc -p ./" + }, + "contributes": { + "commands": [ + { + "category": "Azure CLI", + "command": "ms-azurecli.runLineInTerminal", + "title": "%runLineInTerminal.title%" + }, + { + "category": "Azure CLI", + "command": "ms-azurecli.runLineInEditor", + "title": "%runLineInEditor.title%" + }, + { + "category": "Azure CLI", + "command": "ms-azurecli.toggleLiveQuery", + "title": "%toggleLiveQuery.title%" + }, + { + "category": "Azure CLI", + "command": "ms-azurecli.installAzureCLI", + "title": "%installAzureCLI.title%" + } + ], + "configuration": { + "properties": { + "azureCLI.lineContinuationCharacter": { + "default": "", + "description": "%azureCLI.lineContinuationCharacter.description%", + "scope": "resource", + "type": "string" + }, + "azureCLI.showResultInNewEditor": { + "default": false, + "description": "%azureCLI.showResultInNewEditor.description%", + "scope": "resource", + "type": "boolean" + } + }, + "title": "%configuration.title%", + "type": "object" + }, + "grammars": [ + { + "language": "azcli", + "path": "./syntaxes/azcli.tmLanguage.json", + "scopeName": "source.azcli" + } + ], + "keybindings": [ + { + "command": "ms-azurecli.runLineInTerminal", + "key": "ctrl+'", + "mac": "cmd+'", + "when": "editorTextFocus && editorLangId == 'azcli'" + }, + { + "command": "ms-azurecli.runLineInEditor", + "key": "ctrl+shift+'", + "mac": "cmd+shift+'", + "when": "editorTextFocus && editorLangId == 'azcli'" + }, + { + "command": "ms-azurecli.toggleLiveQuery", + "key": "ctrl+;", + "mac": "cmd+;", + "when": "editorTextFocus && editorLangId == 'azcli'" + } + ], + "languages": [ + { + "aliases": [ + "Azure CLI Scrapbook", + "azcli" + ], + "configuration": "./language-configuration.json", + "extensions": [ + ".azcli" + ], + "id": "azcli" + } + ], + "menus": { + "commandPalette": [ + { + "command": "ms-azurecli.runLineInTerminal", + "when": "editorLangId == 'azcli'" + }, + { + "command": "ms-azurecli.runLineInEditor", + "when": "editorLangId == 'azcli'" + }, + { + "command": "ms-azurecli.toggleLiveQuery", + "when": "editorLangId == 'azcli'" + } + ], + "editor/context": [ + { + "command": "ms-azurecli.runLineInTerminal", + "group": "2_run", + "when": "editorLangId == 'azcli'" + }, + { + "command": "ms-azurecli.runLineInEditor", + "group": "2_run", + "when": "editorLangId == 'azcli'" + } + ] + } + }, + "dependencies": { + "elegant-spinner": "2.0.0", + "jmespath": "0.15.0", + "semver": "7.5.2" + }, + "devDependencies": { + "@types/jmespath": "0.15.0", + "@types/node": "10.14.1", + "@types/semver": "5.5.0" + }, + "icon": "images/azure_icon.png", + "l10n": "./l10n" } diff --git a/service/tests/__init__.py b/service/tests/__init__.py deleted file mode 100644 index 34913fb..0000000 --- a/service/tests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- diff --git a/service/tests/test_tooling.py b/service/tests/test_tooling.py deleted file mode 100644 index 8ceffd1..0000000 --- a/service/tests/test_tooling.py +++ /dev/null @@ -1,129 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -# pylint: skip-file -import unittest -import collections -try: - collectionsAbc = collections.abc -except AttributeError: - collectionsAbc = collections - -from azservice.tooling import GLOBAL_ARGUMENTS, initialize, load_command_table, get_help, get_current_subscription, get_configured_defaults, get_defaults, is_required, run_argument_value_completer, get_arguments - -TEST_GROUP = 'webapp' -TEST_COMMAND = 'webapp create' -TEST_ARGUMENT = 'plan' -TEST_ARGUMENT_OPTIONS = ['--plan', '-p'] -TEST_OPTIONAL_ARGUMENT = 'runtime' -TEST_GLOBAL_ARGUMENT = 'output' -TEST_GLOBAL_ARGUMENT_OPTIONS = ['--output', '-o'] -TEST_ARGUMENT_WITH_DEFAULT = 'deployment_source_branch' -TEST_ARGUMENT_WITHOUT_DEFAULT = 'deployment_source_url' -TEST_COMMAND_WITH_CHOICES = 'appservice plan create' -TEST_ARGUMENT_WITH_CHOICES = 'sku' -TEST_COMMAND_WITH_COMPLETER = 'account set' -TEST_ARGUMENT_WITH_COMPLETER = 'subscription' - - -class ToolingTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - initialize() - cls.command_table = load_command_table() - - @classmethod - def tearDownClass(cls): - cls.command_table = None - - def test_group_help(self): - help = get_help(TEST_GROUP) - self.assertIsNotNone(help) - self.assertTrue(help.get('short-summary')) - - def test_command(self): - command = self.command_table.get(TEST_COMMAND) - self.assertIsNotNone(command) - self.assertEqual(TEST_COMMAND, command.name) - - def test_command_help(self): - help = get_help(TEST_COMMAND) - self.assertIsNotNone(help) - self.assertTrue(help.get('short-summary')) - examples = help.get('examples') - self.assertNotEqual(0, len(examples)) - self.assertTrue(examples[0]['name']) - self.assertTrue(examples[0]['text']) - - def test_argument(self): - command = self.command_table.get(TEST_COMMAND) - self.assertIsNotNone(command) - argument = get_arguments(command).get(TEST_ARGUMENT) - self.assertIsNotNone(argument) - self.assertSequenceEqual(TEST_ARGUMENT_OPTIONS, argument.options_list) - - def test_argument_help(self): - command = self.command_table.get(TEST_COMMAND) - self.assertIsNotNone(command) - argument = get_arguments(command).get(TEST_ARGUMENT) - self.assertIsNotNone(argument) - self.assertTrue(argument.type.settings.get('help')) - - def test_global_argument(self): - argument = GLOBAL_ARGUMENTS.get(TEST_GLOBAL_ARGUMENT) - self.assertIsNotNone(argument) - self.assertSequenceEqual(TEST_GLOBAL_ARGUMENT_OPTIONS, argument['options']) - self.assertTrue(argument['help']) - self.assertNotEqual(0, len(argument['choices'])) - - def test_required_argument(self): - command = self.command_table.get(TEST_COMMAND) - self.assertIsNotNone(command) - self.assertTrue(is_required(get_arguments(command).get(TEST_ARGUMENT))) - self.assertFalse(is_required(get_arguments(command).get(TEST_OPTIONAL_ARGUMENT))) - - def test_is_linux_optional(self): - command = self.command_table.get('appservice plan create') - self.assertIsNotNone(command) - self.assertFalse(is_required(get_arguments(command).get('is_linux'))) - - def test_argument_defaults(self): - command = self.command_table.get(TEST_COMMAND) - self.assertIsNotNone(command) - defaults = get_defaults(get_arguments(command)) - self.assertIsNotNone(defaults) - self.assertTrue(defaults.get(TEST_ARGUMENT_WITH_DEFAULT)) - self.assertFalse(defaults.get(TEST_ARGUMENT_WITHOUT_DEFAULT)) - - def test_argument_choices(self): - command = self.command_table.get(TEST_COMMAND_WITH_CHOICES) - self.assertIsNotNone(command) - argument = get_arguments(command)[TEST_ARGUMENT_WITH_CHOICES] - self.assertIsNotNone(argument) - self.assertIsNotNone(argument.choices) - self.assertIsNone(argument.completer) - self.assertNotEqual(0, len(argument.choices)) - - def test_argument_completer(self): - command = self.command_table.get(TEST_COMMAND_WITH_COMPLETER) - self.assertIsNotNone(command) - argument = get_arguments(command)[TEST_ARGUMENT_WITH_COMPLETER] - self.assertIsNotNone(argument) - self.assertIsNone(argument.choices) - self.assertIsNotNone(argument.completer) - values = run_argument_value_completer(command, argument, {}) - self.assertTrue(isinstance(values, collectionsAbc.Sequence)) - - def test_current_subscription(self): - subscription = get_current_subscription() - self.assertTrue(subscription is None or isinstance(subscription, str)) - - def test_configured_defaults(self): - defaults = get_configured_defaults() - self.assertTrue(isinstance(defaults, dict)) - - -if __name__ == '__main__': - unittest.main() diff --git a/src/azService.ts b/src/azService.ts deleted file mode 100644 index eaf0a64..0000000 --- a/src/azService.ts +++ /dev/null @@ -1,206 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { spawn, ChildProcess, SpawnOptions } from 'child_process'; -import { join } from 'path'; -import * as semver from 'semver'; - -import { exec, realpath, exists, readdir } from './utils'; - -const isWindows = process.platform === 'win32'; - -export type CompletionKind = 'group' | 'command' | 'argument_name' | 'argument_value' | 'snippet'; - -export interface Completion { - name: string; - kind: CompletionKind; - detail?: string; - documentation?: string; - snippet?: string; - sortText?: string; -} - -export type Arguments = Record; - -export interface CompletionQuery { - subcommand?: string; - argument?: string; - arguments?: Arguments -} - -export interface Status { - message: string; -} - -interface StatusQuery { - request: 'status'; -} - -export interface HoverText { - paragraphs: (string | { language: string; value: string })[]; -} - -export interface Command { - subcommand: string; - argument?: string; -} - -interface HoverQuery { - request: 'hover'; - command: Command; -} - -interface Message { - sequence: number; - data: T; -} - -export class AzService { - - private process: Promise | undefined; - private data = ''; - private listeners: { [sequence: number]: ((err: undefined | any, response: Message | undefined) => void); } = {}; - private nextSequenceNumber = 1; - - constructor(azNotFound: (wrongVersion: boolean) => void) { - this.getProcess() - .catch(err => { - console.log(err); - azNotFound(err === 'wrongVersion'); - }); - } - - async getCompletions(query: CompletionQuery, onCancel: (handle: () => void) => void): Promise { - try { - return this.send(query, onCancel); - } catch (err) { - console.error(err); - return []; - } - } - - async getStatus(): Promise { - return this.send({ request: 'status' }); - } - - async getHover(command: Command, onCancel: (handle: () => void) => void): Promise { - return this.send({ - request: 'hover', - command - }, onCancel); - } - - private async send(data: T, onCancel?: (handle: () => void) => void): Promise { - const process = await this.getProcess(); - return new Promise((resolve, reject) => { - if (onCancel) { - onCancel(() => reject('canceled')); - } - const sequence = this.nextSequenceNumber++; - this.listeners[sequence] = (err, response) => { - if (err) { - reject(err); - } else { - try { - resolve(response!.data); - } catch (err) { - reject(err); - } - } - }; - const request: Message = { sequence, data }; - const str = JSON.stringify(request); - process.stdin.write(str + '\n', 'utf8'); - }); - } - - private async getProcess(): Promise { - if (this.process) { - return this.process; - } - return this.process = (async () => { - const { stdout } = await exec('az --version'); - let version = ( - /azure-cli\s+\(([^)]+)\)/m.exec(stdout) - || /azure-cli\s+(\S+)/m.exec(stdout) - || [] - )[1]; - if (version) { - const r = /[^-][a-z]/ig; - if (r.exec(version)) { - version = version.substr(0, r.lastIndex - 1) + '-' + version.substr(r.lastIndex - 1); - } - } - if (version && semver.valid(version) && !semver.gte(version, '2.0.5')) { - throw 'wrongVersion'; - } - const pythonLocation = (/^Python location '([^']*)'/m.exec(stdout) || [])[1]; - const processOptions = await this.getSpawnProcessOptions(); - return this.spawn(pythonLocation, processOptions); - })().catch(err => { - this.process = undefined; - throw err; - }); - } - - private async getSpawnProcessOptions() { - if (process.platform === 'darwin') { - try { - const which = await exec('which az'); - const binPath = await realpath(which.stdout.trim()); - const cellarBasePath = '/usr/local/Cellar/azure-cli/'; - if (binPath.startsWith(cellarBasePath)) { - const installPath = binPath.substr(0, binPath.indexOf('/', cellarBasePath.length)); - const libPath = `${installPath}/libexec/lib`; - const entries = await readdir(libPath); - for (const entry of entries) { - const packagesPath = `${libPath}/${entry}/site-packages`; - if (await exists(packagesPath)) { - return { env: { 'PYTHONPATH': packagesPath } }; - } - } - } - } catch (err) { - console.error(err); - } - } - return undefined; - } - - private spawn(pythonLocation: string, processOptions?: SpawnOptions) { - const process = spawn(join(__dirname, `../../service/az-service${isWindows ? '.bat' : ''}`), [pythonLocation], processOptions); - process.stdout.setEncoding('utf8'); - process.stdout.on('data', data => { - this.data += data; - const nl = this.data.indexOf('\n'); - if (nl !== -1) { - const line = this.data.substr(0, nl); - this.data = this.data.substr(nl + 1); - const response = JSON.parse(line); - const listener = this.listeners[response.sequence]; - if (listener) { - delete this.listeners[response.sequence]; - listener(undefined, response); - } - } - }); - process.stderr.setEncoding('utf8'); - process.stderr.on('data', data => { - console.error(data); - }); - process.on('error', err => { - console.error(err); - }); - process.on('exit', (code, signal) => { - console.error(`Exit code ${code}, signal ${signal}`); - this.process = undefined; - for (const sequence in this.listeners) { - const listener = this.listeners[sequence]; - delete this.listeners[sequence]; - listener(`Python process terminated with exit code ${code}, signal ${signal}.`, undefined); - } - }); - return process; - } -} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts deleted file mode 100644 index 5708bf2..0000000 --- a/src/extension.ts +++ /dev/null @@ -1,486 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as jmespath from 'jmespath'; -import { HoverProvider, Hover, SnippetString, StatusBarAlignment, StatusBarItem, ExtensionContext, TextDocument, TextDocumentChangeEvent, Disposable, TextEditor, Selection, languages, commands, Range, ViewColumn, Position, CancellationToken, ProviderResult, CompletionItem, CompletionList, CompletionItemKind, CompletionItemProvider, window, workspace, env, Uri, WorkspaceEdit, l10n, } from 'vscode'; -import * as process from "process"; - -import { AzService, CompletionKind, Arguments, Status } from './azService'; -import { parse, findNode } from './parser'; -import { exec } from './utils'; -import * as spinner from 'elegant-spinner'; - -export function activate(context: ExtensionContext) { - const azService = new AzService(azNotFound); - context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(azService), ' ')); - context.subscriptions.push(languages.registerHoverProvider('azcli', new AzHoverProvider(azService))); - const status = new StatusBarInfo(azService); - context.subscriptions.push(status); - context.subscriptions.push(new RunLineInTerminal()); - context.subscriptions.push(new RunLineInEditor(status)); - context.subscriptions.push(commands.registerCommand('ms-azurecli.installAzureCLI', installAzureCLI)); -} - -const completionKinds: Record = { - group: CompletionItemKind.Module, - command: CompletionItemKind.Function, - argument_name: CompletionItemKind.Variable, - argument_value: CompletionItemKind.EnumMember, - snippet: CompletionItemKind.Snippet -}; - -class AzCompletionItemProvider implements CompletionItemProvider { - - constructor(private azService: AzService) { - } - - provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - const line = document.lineAt(position).text; - const parsed = parse(line); - const start = parsed.subcommand[0]; - if (start && start.offset + start.length < position.character && start.text !== 'az') { - return; - } - const node = findNode(parsed, position.character - 1); - if (node && node.kind === 'comment') { - return; - } - // TODO: Use the above instead of parsing again. - const upToCursor = line.substr(0, position.character); - const rawSubcommand = (/^\s*(([^-\s][^\s]*\s+)*)/.exec(upToCursor) || [])[1]; - if (typeof rawSubcommand !== 'string') { - return Promise.resolve([]); - } - const subcommand = rawSubcommand.trim() - .split(/\s+/); - const args = this.getArguments(line); - const argument = (/\s(--?[^\s]+)\s+[^-\s]*$/.exec(upToCursor) || [])[1]; - const prefix = (/(^|\s)([^\s]*)$/.exec(upToCursor) || [])[2]; - const lead = /^-*/.exec(prefix)![0]; - return this.azService.getCompletions(subcommand[0] === 'az' ? { subcommand: subcommand.slice(1).join(' '), argument, arguments: args } : {}, token.onCancellationRequested) - .then(completions => completions.map(({ name, kind, detail, documentation, snippet, sortText }) => { - const item = new CompletionItem(name, completionKinds[kind]); - if (snippet) { - item.insertText = new SnippetString(snippet); - } else if (lead) { - item.insertText = name.substr(lead.length); - } - if (detail) { - item.detail = detail; - } - if (documentation) { - item.documentation = documentation; - } - if (sortText) { - item.sortText = sortText; - } - return item; - })); - } - - private getArguments(line: string) { - const args: Arguments = {}; - let name: string | undefined; - for (const match of allMatches(/-[^\s"']*|"[^"]*"|'[^']*'|[^\s"']+/g, line, 0)) { - if (match.startsWith('-')) { - name = match as string; - if (!(name in args)) { - args[name] = null; - } - } else { - if (name) { - args[name] = match; - } - name = undefined; - } - } - return args; - } -} - -class AzHoverProvider implements HoverProvider { - - constructor(private azService: AzService) { - } - - provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - const line = document.lineAt(position.line).text; - const command = parse(line); - const list = command.subcommand; - if (list.length && list[0].text === 'az') { - const node = findNode(command, position.character); - if (node) { - if (node.kind === 'subcommand') { - const i = list.indexOf(node); - if (i > 0) { - const subcommand = list.slice(1, i + 1) - .map(node => node.text).join(' '); - return this.azService.getHover({ subcommand }, token.onCancellationRequested) - .then(text => text && new Hover(text.paragraphs, new Range(position.line, node.offset, position.line, node.offset + node.length))); - } - } else if (node.kind === 'argument_name') { - const subcommand = command.subcommand.slice(1) - .map(node => node.text).join(' '); - return this.azService.getHover({ subcommand, argument: node.text }, token.onCancellationRequested) - .then(text => text && new Hover(text.paragraphs, new Range(position.line, node.offset, position.line, node.offset + node.length))); - } - } - } - } -} - -class RunLineInTerminal { - - private disposables: Disposable[] = []; - - constructor() { - this.disposables.push(commands.registerTextEditorCommand('ms-azurecli.runLineInTerminal', editor => this.run(editor))); - } - - private run(editor: TextEditor) { - return commands.executeCommand('workbench.action.terminal.runSelectedText'); - } - - dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } -} - -class RunLineInEditor { - - private resultDocument: TextDocument | undefined; - private parsedResult: object | undefined; - private queryEnabled = false; - private query: string | undefined; - private disposables: Disposable[] = []; - private commandRunningStatusBarItem: StatusBarItem; - private statusBarUpdateInterval!: NodeJS.Timer; - private statusBarSpinner = spinner(); - private hideStatusBarItemTimeout! : NodeJS.Timeout; - private statusBarItemText : string = ''; - // using backtick (`) as continuation character on Windows, backslash (\) on other systems - private continuationCharacter : string = process.platform === "win32" ? "`" : "\\"; - - constructor(private status: StatusBarInfo) { - this.disposables.push(commands.registerTextEditorCommand('ms-azurecli.toggleLiveQuery', editor => this.toggleQuery(editor))); - this.disposables.push(commands.registerTextEditorCommand('ms-azurecli.runLineInEditor', editor => this.run(editor))); - this.disposables.push(workspace.onDidCloseTextDocument(document => this.close(document))); - this.disposables.push(workspace.onDidChangeTextDocument(event => this.change(event))); - - this.commandRunningStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); - this.disposables.push(this.commandRunningStatusBarItem); - } - - private runningCommandCount : number = 0; - private run(source: TextEditor) { - this.refreshContinuationCharacter(); - const command = this.getSelectedCommand(source); - if (command.length > 0) { - this.runningCommandCount += 1; - const t0 = Date.now(); - if (this.runningCommandCount === 1) { - this.statusBarItemText = l10n.t('Azure CLI: Waiting for response'); - this.statusBarUpdateInterval = setInterval(() => { - if (this.runningCommandCount === 1) { - this.commandRunningStatusBarItem.text = `${this.statusBarItemText} ${this.statusBarSpinner()}`; - } - else { - this.commandRunningStatusBarItem.text = `${this.statusBarItemText} [${this.runningCommandCount}] ${this.statusBarSpinner()}`; - } - }, 50); - } - this.commandRunningStatusBarItem.show(); - clearTimeout(this.hideStatusBarItemTimeout); - - this.parsedResult = undefined; - this.query = undefined; // TODO - return this.findResultDocument() - .then(document => window.showTextDocument(document, ViewColumn.Two, true)) - .then(target => replaceContent(target, JSON.stringify({ [l10n.t('Running command')]: command }) + '\n') - .then(() => exec(command)) - .then(({ stdout }) => stdout, ({ stdout, stderr }) => JSON.stringify({ stderr, stdout }, null, ' ')) - .then(content => replaceContent(target, content) - .then(() => this.parsedResult = JSON.parse(content)) - .then(undefined, err => {}) - ) - .then(() => this.commandFinished(t0)) - ) - .then(undefined, console.error); - } - } - - private refreshContinuationCharacter() { - // the continuation character setting can be changed after the extension is loaded - const settingsContinuationCharacter = workspace.getConfiguration('azureCLI', null).get('lineContinuationCharacter', ""); - if (settingsContinuationCharacter.length > 0) { - this.continuationCharacter = settingsContinuationCharacter; - } - else { - this.continuationCharacter = process.platform === "win32" ? "`" : "\\"; - } - } - - private getSelectedCommand(source: TextEditor) { - const commandPrefix = "az"; - - if (source.selection.isEmpty) { - var lineNumber = source.selection.active.line; - if (source.document.lineAt(lineNumber).text.length === 0) { - window.showInformationMessage(l10n.t("Please put the cursor on a line that contains a command (or part of a command).")); - return ""; - } - - // look upwards find the start of the command (if necessary) - while(!source.document.lineAt(lineNumber).text.trim().toLowerCase().startsWith(commandPrefix)) { - lineNumber--; - } - - // this will be the first (maybe only) line of the command - var command = this.stripComments(source.document.lineAt(lineNumber).text); - - while (command.trim().endsWith(this.continuationCharacter)) { - // concatenate all lines into a single command - lineNumber ++; - command = command.trim().slice(0, -1) + this.stripComments(source.document.lineAt(lineNumber).text); - } - return command; - } - else { - // execute only the selected text - const selectionStart = source.selection.start; - const selectionEnd = source.selection.end; - if (selectionStart.line === selectionEnd.line) { - // single line command - return this.stripComments(source.document.getText(new Range(selectionStart, selectionEnd))); - } - else { - // multiline command - command = this.stripComments(source.document.lineAt(selectionStart.line).text.substring(selectionStart.character)); - for (let index = selectionStart.line+1; index <= selectionEnd.line; index++) { - if (command.trim().endsWith(this.continuationCharacter)) { - command = command.trim().slice(0, -1); // remove continuation character from command - } - - var line = this.stripComments(source.document.lineAt(index).text); - - if (line.trim().toLowerCase().startsWith(commandPrefix)) { - window.showErrorMessage(l10n.t("Multiple command selection not supported")); - return ""; - } - - // append this line to the command string - if (index === selectionEnd.line) { - command = command + line.substring(0, selectionEnd.character); // only append up to the end of the selection - } - else { - command = command + line; - } - } - return command; - } - } - } - - private stripComments(text: string) { - if (text.trim().startsWith("#")) { - return this.continuationCharacter; // don't let a whole line comment terminate a sequence of command fragments - } - - var i = text.indexOf("#"); - if (i !== -1) { - // account for hash characters that are embedded in strings in the JMESPath query - while (this.isEmbeddedInString(text, i)) { - i = text.indexOf("#", i + 1); // find next # - } - return text.substring(0, i); - } - - // no comment found - return text; - } - - // true if the specified position is in a string literal (surrounded by single quotes) - private isEmbeddedInString(text: string, position: number) : boolean { - var stringStart = text.indexOf("'"); // start of string literal - if (stringStart !== -1) { - while (stringStart !== -1) { - var stringEnd = text.indexOf("'", stringStart + 1); // end of string literal - if ((stringEnd !== -1) && (stringStart < position) && (stringEnd > position)) { - return true; // the given position is embedded in a string literal - } - stringStart = text.indexOf("'", stringEnd + 1); - } - } - return false; - } - - private commandFinished(startTime: number) { - this.runningCommandCount -= 1 - this.statusBarItemText = l10n.t('Azure CLI: Executed in {0} milliseconds', Date.now() - startTime); - this.commandRunningStatusBarItem.text = this.statusBarItemText; - - if (this.runningCommandCount === 0) { - clearInterval(this.statusBarUpdateInterval); - - // hide status bar item after 10 seconds to keep status bar uncluttered - this.hideStatusBarItemTimeout = setTimeout(() => this.commandRunningStatusBarItem.hide(), 10000); - } - } - - private toggleQuery(source: TextEditor) { - this.queryEnabled = !this.queryEnabled; - this.status.liveQuery = this.queryEnabled; - this.status.update(); - this.updateResult(); - } - - private findResultDocument() { - const showResultInNewEditor = workspace.getConfiguration('azureCLI', null).get('showResultInNewEditor', false) - if (this.resultDocument && !showResultInNewEditor) { - return Promise.resolve(this.resultDocument); - } - return workspace.openTextDocument({ language: 'json' }) - .then(document => this.resultDocument = document); - } - - private close(document: TextDocument) { - if (document === this.resultDocument) { - this.resultDocument = undefined; - } - } - - private change(e: TextDocumentChangeEvent) { - if (e.document.languageId === 'azcli' && e.contentChanges.length === 1) { - const change = e.contentChanges[0]; - const range = change.range; - if (range.start.line === range.end.line) { - const line = e.document.lineAt(range.start.line).text; - const query = this.getQueryArgument(line); - if (query !== this.query) { - this.query = query; - if (this.queryEnabled) { - this.updateResult(); - } - } - } - } - } - - private updateResult() { - if (this.resultDocument && this.parsedResult) { - const resultEditor = window.visibleTextEditors.find(editor => editor.document === this.resultDocument); - if (resultEditor) { - try { - const result = this.queryEnabled && this.query ? jmespath.search(this.parsedResult, this.query) : this.parsedResult; - replaceContent(resultEditor, JSON.stringify(result, null, ' ')) - .then(undefined, console.error); - } catch (err: any) { - if (!(err && err.name === 'ParserError')) { - // console.error(err); Ignore because jmespath sometimes fails on partial queries. - } - } - } - } - } - - private getQueryArgument(line: string) { - return (/\s--query\s+("([^"]*)"|'([^']*)'|([^\s"']+))/.exec(line) as string[] || []) - .filter(group => !!group)[2]; - } - - dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } -} - -class StatusBarInfo { - - private info: StatusBarItem; - private status?: Status; - public liveQuery = false; - - private timer?: NodeJS.Timer; - private disposables: Disposable[] = []; - - constructor(private azService: AzService) { - this.disposables.push(this.info = window.createStatusBarItem(StatusBarAlignment.Left)); - this.disposables.push(window.onDidChangeActiveTextEditor(() => this.update())); - this.disposables.push({ dispose: () => this.timer && clearTimeout(this.timer) }); - this.refresh() - .catch(console.error); - } - - public async refresh() { - if (this.timer) { - clearTimeout(this.timer); - } - this.status = await this.azService.getStatus(); - this.update(); - this.timer = setTimeout(() => { - this.refresh() - .catch(console.error); - }, 5000); - } - - public update() { - const texts: string[] = []; - if (this.status && this.status.message) { - texts.push(this.status.message); - } - if (this.liveQuery) { - texts.push('Live Query'); - } - this.info.text = texts.join(', '); - const editor = window.activeTextEditor; - const show = this.info.text && editor && editor.document.languageId === 'azcli'; - this.info[show ? 'show' : 'hide'](); - } - - dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } -} - -function allMatches(regex: RegExp, string: string, group: number) { - return { - [Symbol.iterator]: function* () { - let m; - while (m = regex.exec(string)) { - yield m[group]; - } - } - } -} - -function replaceContent(editor: TextEditor, content: string) { - const document = editor.document; - const all = new Range(new Position(0, 0), document.lineAt(document.lineCount - 1).range.end); - const edit = new WorkspaceEdit(); - edit.replace(document.uri, all, content); - return workspace.applyEdit(edit) - .then(() => editor.selections = [new Selection(0, 0, 0, 0)]); -} - -async function azNotFound(wrongVersion: boolean): Promise { - const message = - wrongVersion - ? l10n.t("'az' >= 2.0.5 required, please update your installation.") - : l10n.t("'az' not found on PATH, please make sure it is installed."); - const result = await window.showInformationMessage(message, - { - title: l10n.t('Documentation'), - run: installAzureCLI - } - ); - if (result && result.run) { - result.run(); - } -} - -function installAzureCLI() { - env.openExternal(Uri.parse('https://aka.ms/GetTheAzureCLI')); -} - -export function deactivate() { -} diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index d407190..0000000 --- a/src/parser.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { never } from './utils'; - -export type TokenKind = 'subcommand' | 'argument_name' | 'argument_value' | 'comment'; - -export interface Token { - kind: TokenKind; - offset: number; - length: number; - text: string; -} - -export interface Argument { - name?: Token; - value?: Token; -} - -export interface Command { - tokens: Token[]; - subcommand: Token[]; - arguments: Argument[]; - comment?: Token; -} - -export function parse(line: string) { - const regex = /"[^"]*"|'[^']*'|\#.*|[^\s"'#]+/g; - const tokens: Token[] = []; - let subcommand = true; - let m; - while (m = regex.exec(line)) { - const text = m[0]; - const length = text.length; - const isArgument = text.startsWith('-'); - const isComment = text.startsWith('#'); - if (isArgument || isComment) { - subcommand = false; - } - tokens.push({ - kind: subcommand ? 'subcommand' : isArgument ? 'argument_name' : - isComment ? 'comment' : 'argument_value', - offset: regex.lastIndex - length, - length, - text - }); - } - - const command: Command = { - tokens, - subcommand: [], - arguments: [] - }; - const args = command.arguments; - - for (const token of tokens) { - switch (token.kind) { - case 'subcommand': - command.subcommand.push(token); - break; - case 'argument_name': - args.push({ name: token }); - break; - case 'argument_value': - if (args.length && !('value' in args[args.length - 1])) { - args[args.length - 1].value = token; - } else { - args.push({ value: token }); - } - break; - case 'comment': - command.comment = token; - break; - default: - never(token.kind); - } - } - - return command; -} - -export function findNode(command: Command, offset: number) { - return command.tokens.find(token => token.offset <= offset && token.offset + token.length > offset); -} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index ede4cf2..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as cp from 'child_process'; -import * as fs from 'fs'; - -export interface ExecResult { - error: Error | null; - stdout: string; - stderr: string; -} - -export function exec(command: string) { - return new Promise((resolve, reject) => { - cp.exec(command, (error, stdout, stderr) => { - (error ? reject : resolve)({ error, stdout, stderr }); - }); - }); -} - -export function realpath(path: string) { - return new Promise((resolve, reject) => { - fs.realpath(path, (error, resolvedPath) => { - if (error) { - reject(error); - } else { - resolve(resolvedPath); - } - }); - }); -} - -export function exists(path: string) { - return new Promise(resolve => { - fs.exists(path, resolve); - }); -} - -export function readdir(path: string) { - return new Promise((resolve, reject) => { - fs.readdir(path, (error, files) => { - if (error) { - reject(error); - } else { - resolve(files); - } - }); - }); -} - -export function never(n: never) { - throw new Error(`Should not happen: ${n}`); -}