diff --git a/examples/webapp.azcli b/examples/webapp.azcli index 1e4b520..c077740 100644 --- a/examples/webapp.azcli +++ b/examples/webapp.azcli @@ -1,5 +1,5 @@ # Login and set default subscription -az login +az login az account set --subscription "Try Out Subscription" # Create resource group @@ -8,11 +8,14 @@ az configure --defaults group=tryout-group # Create web app az appservice plan create --name tryout-plan --is-linux --sku B2 -az webapp create --name tryout-web --plan tryout-plan +az webapp create --name tryout-web --plan tryout-plan --runtime "node|6.2" az configure --defaults web=tryout-web # Manual deployment from source repository -az webapp deployment source config --repo-url "https://github.com/Azure-Samples/app-service-web-html-get-started.git" --branch master --manual-integration +az webapp deployment source config \ + --repo-url "https://github.com/Azure-Samples/app-service-web-html-get-started.git" \ + --branch master \ + --manual-integration # Or from docker image az webapp config container set --docker-custom-image-name tutum/hello-world az webapp browse diff --git a/package.json b/package.json index a933f68..3c1451e 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,12 @@ "default": false, "scope": "resource", "description": "Controls whether showing the result from running an Azure CLI command in an editor should always create a new editor." + }, + "azureCLI.lineContinuationCharacter": { + "type": "string", + "default": "", + "scope": "resource", + "description": "Override the default continuation character (backtick [`] on Windows otherwise backslash [\\]) used for multiline commands" } } }, diff --git a/src/extension.ts b/src/extension.ts index ec12af6..94ebe40 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,8 @@ * 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 } from 'vscode'; +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, } from 'vscode'; +import * as process from "process"; import { AzService, CompletionKind, Arguments, Status } from './azService'; import { parse, findNode } from './parser'; @@ -158,6 +159,8 @@ class RunLineInEditor { 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))); @@ -166,56 +169,158 @@ class RunLineInEditor { this.disposables.push(workspace.onDidChangeTextDocument(event => this.change(event))); this.commandRunningStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); - this.disposables.push(this.commandRunningStatusBarItem); + this.disposables.push(this.commandRunningStatusBarItem); } private runningCommandCount : number = 0; private run(source: TextEditor) { - this.runningCommandCount += 1; - const t0 = Date.now(); - if (this.runningCommandCount === 1) - { - this.statusBarItemText = `Azure CLI: Waiting for response`; - this.statusBarUpdateInterval = setInterval(() => { - if (this.runningCommandCount === 1) - { - this.commandRunningStatusBarItem.text = `${this.statusBarItemText} ${this.statusBarSpinner()}`; + this.refreshContinuationCharacter(); + const command = this.getSelectedCommand(source); + if (command.length > 0) { + this.runningCommandCount += 1; + const t0 = Date.now(); + if (this.runningCommandCount === 1) { + this.statusBarItemText = `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({ '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("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("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; + } } - else - { - this.commandRunningStatusBarItem.text = `${this.statusBarItemText} [${this.runningCommandCount}] ${this.statusBarSpinner()}`; + 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 } - }, 50); + stringStart = text.indexOf("'", stringEnd + 1); + } } - this.commandRunningStatusBarItem.show(); - clearTimeout(this.hideStatusBarItemTimeout); - - this.parsedResult = undefined; - this.query = undefined; // TODO - const cursor = source.selection.active; - const line = source.document.lineAt(cursor).text; - return this.findResultDocument() - .then(document => window.showTextDocument(document, ViewColumn.Two, true)) - .then(target => replaceContent(target, JSON.stringify({ 'Running command': line }) + '\n') - .then(() => exec(line)) - .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); + return false; } - private commandFinished(startTime: number) - { + private commandFinished(startTime: number) { this.runningCommandCount -= 1 this.statusBarItemText = 'Azure CLI: Executed in ' + (Date.now() - startTime) + ' milliseconds'; this.commandRunningStatusBarItem.text = this.statusBarItemText; - if (this.runningCommandCount === 0) - { + if (this.runningCommandCount === 0) { clearInterval(this.statusBarUpdateInterval); // hide status bar item after 10 seconds to keep status bar uncluttered