Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions examples/webapp.azcli
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Login and set default subscription
az login
az login
az account set --subscription "Try Out Subscription"

# Create resource group
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
181 changes: 143 additions & 38 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)));
Expand All @@ -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<string>('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<any>("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<any>("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
Expand Down