diff --git a/Source/azService.ts b/Source/azService.ts index eaf0a64..8a2ba19 100644 --- a/Source/azService.ts +++ b/Source/azService.ts @@ -2,205 +2,245 @@ * 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 { type ChildProcess, type SpawnOptions, spawn } from "child_process"; +import { join } from "path"; +import * as semver from "semver"; -import { exec, realpath, exists, readdir } from './utils'; +import { exec, exists, readdir, realpath } from "./utils"; -const isWindows = process.platform === 'win32'; +const isWindows = process.platform === "win32"; -export type CompletionKind = 'group' | 'command' | 'argument_name' | 'argument_value' | 'snippet'; +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; + 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 + subcommand?: string; + argument?: string; + arguments?: Arguments; } export interface Status { - message: string; + message: string; } interface StatusQuery { - request: 'status'; + request: "status"; } export interface HoverText { - paragraphs: (string | { language: string; value: string })[]; + paragraphs: (string | { language: string; value: string })[]; } export interface Command { - subcommand: string; - argument?: string; + subcommand: string; + argument?: string; } interface HoverQuery { - request: 'hover'; - command: Command; + request: "hover"; + command: Command; } interface Message { - sequence: number; - data: T; + 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 + 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]/gi; + 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; + } +} diff --git a/Source/extension.ts b/Source/extension.ts index 5708bf2..8a57b0f 100644 --- a/Source/extension.ts +++ b/Source/extension.ts @@ -2,485 +2,719 @@ * 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 jmespath from "jmespath"; 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'; +import { + type CancellationToken, + CompletionItem, + CompletionItemKind, + type CompletionItemProvider, + type CompletionList, + type Disposable, + type ExtensionContext, + Hover, + type HoverProvider, + Position, + type ProviderResult, + Range, + Selection, + SnippetString, + StatusBarAlignment, + type StatusBarItem, + type TextDocument, + type TextDocumentChangeEvent, + type TextEditor, + Uri, + ViewColumn, + WorkspaceEdit, + commands, + env, + l10n, + languages, + window, + workspace, +} from "vscode"; + +import * as spinner from "elegant-spinner"; +import { + type Arguments, + AzService, + type CompletionKind, + type Status, +} from "./azService"; +import { findNode, parse } from "./parser"; +import { exec } from "./utils"; 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 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 + 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; - } + 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))); - } - } - } - } + 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()); - } + 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()); - } + 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 = ""; + // 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 = 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()); - } + 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]; - } - } - } + 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)]); + 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(); - } + 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')); + env.openExternal(Uri.parse("https://aka.ms/GetTheAzureCLI")); } -export function deactivate() { -} +export function deactivate() {} diff --git a/Source/parser.ts b/Source/parser.ts index d407190..cb7adcd 100644 --- a/Source/parser.ts +++ b/Source/parser.ts @@ -1,81 +1,93 @@ -import { never } from './utils'; +import { never } from "./utils"; -export type TokenKind = 'subcommand' | 'argument_name' | 'argument_value' | 'comment'; +export type TokenKind = + | "subcommand" + | "argument_name" + | "argument_value" + | "comment"; export interface Token { - kind: TokenKind; - offset: number; - length: number; - text: string; + kind: TokenKind; + offset: number; + length: number; + text: string; } export interface Argument { - name?: Token; - value?: Token; + name?: Token; + value?: Token; } export interface Command { - tokens: Token[]; - subcommand: Token[]; - arguments: Argument[]; - comment?: Token; + 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; + 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, + }); + } - 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); - } - } + const command: Command = { + tokens, + subcommand: [], + arguments: [], + }; + const args = command.arguments; - return command; + 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); + return command.tokens.find( + (token) => + token.offset <= offset && token.offset + token.length > offset, + ); } diff --git a/Source/utils.ts b/Source/utils.ts index ede4cf2..a6c366f 100644 --- a/Source/utils.ts +++ b/Source/utils.ts @@ -1,50 +1,50 @@ -import * as cp from 'child_process'; -import * as fs from 'fs'; +import * as cp from "child_process"; +import * as fs from "fs"; export interface ExecResult { - error: Error | null; - stdout: string; - stderr: string; + 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 }); - }); - }); + 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); - } - }); - }); + 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); - }); + 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); - } - }); - }); + 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}`); + throw new Error(`Should not happen: ${n}`); } diff --git a/language-configuration.json b/language-configuration.json index 370e5be..15a1b3f 100644 --- a/language-configuration.json +++ b/language-configuration.json @@ -1,24 +1,20 @@ { - "comments": { - "lineComment": "#" - }, - "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] - ], - "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] - ], - "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] - ] -} \ No newline at end of file + "comments": { + "lineComment": "#" + }, + "brackets": [["{", "}"], ["[", "]"], ["(", ")"]], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ] +} diff --git a/package.json b/package.json index 04fe02d..4015b50 100644 --- a/package.json +++ b/package.json @@ -95,14 +95,9 @@ ], "languages": [ { - "aliases": [ - "Azure CLI Scrapbook", - "azcli" - ], + "aliases": ["Azure CLI Scrapbook", "azcli"], "configuration": "./language-configuration.json", - "extensions": [ - ".azcli" - ], + "extensions": [".azcli"], "id": "azcli" } ], diff --git a/package.nls.json b/package.nls.json index 4617932..1a6c000 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,9 +1,9 @@ { - "runLineInTerminal.title": "Run Line in Terminal", - "runLineInEditor.title": "Run Line in Editor", - "toggleLiveQuery.title": "Toggle Live Query", - "installAzureCLI.title": "Install the Azure CLI", - "configuration.title": "Azure CLI Tools Configuration", - "azureCLI.showResultInNewEditor.description": "Controls whether showing the result from running an Azure CLI command in an editor should always create a new editor.", - "azureCLI.lineContinuationCharacter.description": "Override the default continuation character (backtick [`] on Windows otherwise backslash [\\]) used for multiline commands" + "runLineInTerminal.title": "Run Line in Terminal", + "runLineInEditor.title": "Run Line in Editor", + "toggleLiveQuery.title": "Toggle Live Query", + "installAzureCLI.title": "Install the Azure CLI", + "configuration.title": "Azure CLI Tools Configuration", + "azureCLI.showResultInNewEditor.description": "Controls whether showing the result from running an Azure CLI command in an editor should always create a new editor.", + "azureCLI.lineContinuationCharacter.description": "Override the default continuation character (backtick [`] on Windows otherwise backslash [\\]) used for multiline commands" } diff --git a/syntaxes/azcli.tmLanguage.json b/syntaxes/azcli.tmLanguage.json index fa7c4fe..b48b05f 100644 --- a/syntaxes/azcli.tmLanguage.json +++ b/syntaxes/azcli.tmLanguage.json @@ -23,22 +23,28 @@ ], "repository": { "subcommand": { - "patterns": [{ - "name": "storage.type.azcli", - "match": "^([^\\s#-]-+|[^#-])+" - }] + "patterns": [ + { + "name": "storage.type.azcli", + "match": "^([^\\s#-]-+|[^#-])+" + } + ] }, "argumentName": { - "patterns": [{ - "name": "variable.other.azcli", - "match": "-[^\\s#]*\\s*" - }] + "patterns": [ + { + "name": "variable.other.azcli", + "match": "-[^\\s#]*\\s*" + } + ] }, "argumentValueUnquoted": { - "patterns": [{ - "name": "string.unquoted.azcli", - "match": "[^\\s#]+\\s*" - }] + "patterns": [ + { + "name": "string.unquoted.azcli", + "match": "[^\\s#]+\\s*" + } + ] }, "argumentValueSingleQuoted": { "name": "string.quoted.single.azcli", @@ -63,10 +69,12 @@ ] }, "comments": { - "patterns": [{ - "name": "comment.line.hash.azcli", - "match": "\\#.*" - }] + "patterns": [ + { + "name": "comment.line.hash.azcli", + "match": "\\#.*" + } + ] } }, "scopeName": "source.azcli" diff --git a/tsconfig.json b/tsconfig.json index 35b64ef..3302b87 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,13 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "strict": true, - "noUnusedLocals": true, - "outDir": "out", - "lib": [ - "es6" - ], - "sourceMap": true, - "rootDir": "." - }, - "exclude": [ - "node_modules", - ".vscode-test" - ] -} \ No newline at end of file + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "strict": true, + "noUnusedLocals": true, + "outDir": "out", + "lib": ["es6"], + "sourceMap": true, + "rootDir": "." + }, + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/tslint.json b/tslint.json index 1c0afaa..d4cbc27 100644 --- a/tslint.json +++ b/tslint.json @@ -4,9 +4,7 @@ "no-duplicate-variable": true, "curly": true, "class-name": true, - "semicolon": [ - "always" - ], + "semicolon": ["always"], "triple-equals": true } -} \ No newline at end of file +}