diff --git a/extension.bundle.ts b/extension.bundle.ts
index 179ea26d0..89edd9661 100644
--- a/extension.bundle.ts
+++ b/extension.bundle.ts
@@ -46,6 +46,7 @@ export { HoverInfo } from "./src/Hover";
export { httpGet } from './src/httpGet';
export { DefinitionKind, INamedDefinition } from "./src/INamedDefinition";
export { IncorrectArgumentsCountIssue } from "./src/IncorrectArgumentsCountIssue";
+export { InsertItem } from "./src/insertItem";
export { IParameterDefinition } from "./src/IParameterDefinition";
export * from "./src/Language";
export { LanguageServerState } from "./src/languageclient/startArmLanguageServer";
@@ -62,6 +63,7 @@ export { containsArmSchema, getPreferredSchema, isArmSchema } from './src/schema
export * from "./src/survey";
export { TemplatePositionContext } from "./src/TemplatePositionContext";
export { ScopeContext, TemplateScope } from "./src/TemplateScope";
+export { TemplateSectionType } from "./src/TemplateSectionType";
export { FunctionSignatureHelp } from "./src/TLE";
export { JsonOutlineProvider, shortenTreeLabel } from "./src/Treeview";
export { UnrecognizedBuiltinFunctionIssue, UnrecognizedUserFunctionIssue, UnrecognizedUserNamespaceIssue } from "./src/UnrecognizedFunctionIssues";
diff --git a/icons/insertItemDark.svg b/icons/insertItemDark.svg
new file mode 100644
index 000000000..4d9389336
--- /dev/null
+++ b/icons/insertItemDark.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/insertItemLight.svg b/icons/insertItemLight.svg
new file mode 100644
index 000000000..01a9de7d5
--- /dev/null
+++ b/icons/insertItemLight.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/sortDark.svg b/icons/sortDark.svg
new file mode 100644
index 000000000..b00f67f86
--- /dev/null
+++ b/icons/sortDark.svg
@@ -0,0 +1,49 @@
+
+
+
diff --git a/icons/sortLight.svg b/icons/sortLight.svg
new file mode 100644
index 000000000..7090fda76
--- /dev/null
+++ b/icons/sortLight.svg
@@ -0,0 +1,49 @@
+
+
+
diff --git a/package.json b/package.json
index 7760718e2..4bc5e04f6 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,12 @@
"onCommand:azurerm-vscode-tools.openTemplateFile",
"onCommand:azurerm-vscode-tools.codeAction.addAllMissingParameters",
"onCommand:azurerm-vscode-tools.codeAction.addMissingRequiredParameters",
+ "onCommand:azurerm-vscode-tools.insertItem",
+ "onCommand:azurerm-vscode-tools.insertParameter",
+ "onCommand:azurerm-vscode-tools.insertVariable",
+ "onCommand:azurerm-vscode-tools.insertOutput",
+ "onCommand:azurerm-vscode-tools.insertFunction",
+ "onCommand:azurerm-vscode-tools.insertResource",
"onCommand:azurerm-vscode-tools.resetGlobalState"
],
"contributes": {
@@ -166,32 +172,56 @@
"$comment": "============= Template sorting =============",
"category": "Azure Resource Manager Tools",
"title": "Sort Template...",
- "command": "azurerm-vscode-tools.sortTemplate"
+ "command": "azurerm-vscode-tools.sortTemplate",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"category": "Azure Resource Manager Tools",
"title": "Sort Functions",
- "command": "azurerm-vscode-tools.sortFunctions"
+ "command": "azurerm-vscode-tools.sortFunctions",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"category": "Azure Resource Manager Tools",
"title": "Sort Outputs",
- "command": "azurerm-vscode-tools.sortOutputs"
+ "command": "azurerm-vscode-tools.sortOutputs",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"category": "Azure Resource Manager Tools",
"title": "Sort Parameters",
- "command": "azurerm-vscode-tools.sortParameters"
+ "command": "azurerm-vscode-tools.sortParameters",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"category": "Azure Resource Manager Tools",
"title": "Sort Resources",
- "command": "azurerm-vscode-tools.sortResources"
+ "command": "azurerm-vscode-tools.sortResources",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"category": "Azure Resource Manager Tools",
"title": "Sort Variables",
- "command": "azurerm-vscode-tools.sortVariables"
+ "command": "azurerm-vscode-tools.sortVariables",
+ "icon": {
+ "light": "icons/sortLight.svg",
+ "dark": "icons/sortDark.svg"
+ }
},
{
"$comment": "============= Template file commands =============",
@@ -228,6 +258,60 @@
"command": "azurerm-vscode-tools.codeAction.addMissingRequiredParameters",
"enablement": "azurerm-vscode-tools-hasTemplateFile",
"$enablement.comment": "Shows up when it's a param file, but only enabled if there is an associated template file"
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Item...",
+ "command": "azurerm-vscode-tools.insertItem",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Variable...",
+ "command": "azurerm-vscode-tools.insertVariable",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Function...",
+ "command": "azurerm-vscode-tools.insertFunction",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Resource...",
+ "command": "azurerm-vscode-tools.insertResource",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Parameter...",
+ "command": "azurerm-vscode-tools.insertParameter",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
+ },
+ {
+ "category": "Azure Resource Manager Tools",
+ "title": "Insert Output...",
+ "command": "azurerm-vscode-tools.insertOutput",
+ "icon": {
+ "light": "icons/insertItemLight.svg",
+ "dark": "icons/insertItemDark.svg"
+ }
}
],
"menus": {
@@ -252,6 +336,26 @@
"command": "azurerm-vscode-tools.sortVariables",
"when": "never"
},
+ {
+ "command": "azurerm-vscode-tools.insertParameter",
+ "when": "never"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertVariable",
+ "when": "never"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertOutput",
+ "when": "never"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertFunction",
+ "when": "never"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertResource",
+ "when": "never"
+ },
{
"command": "azurerm-vscode-tools.openTemplateFile",
"when": "never"
@@ -275,6 +379,11 @@
"when": "editorLangId==arm-template",
"group": "zzz_arm-template@3"
},
+ {
+ "command": "azurerm-vscode-tools.insertItem",
+ "when": "editorLangId==arm-template",
+ "group": "zzz_arm-template@4"
+ },
{
"$comment": "============= Parameter file commands =============",
"command": "azurerm-vscode-tools.openTemplateFile",
@@ -286,34 +395,104 @@
"view/item/context": [
{
"$comment": "============= Treeview commands =============",
- "command": "azurerm-vscode-tools.sortTemplate",
- "when": "azurerm-vscode-tools.template-outline.active == true",
- "group": "arm-template"
- },
- {
"command": "azurerm-vscode-tools.sortFunctions",
- "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == functions",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == functions",
"group": "arm-template"
},
{
"command": "azurerm-vscode-tools.sortOutputs",
- "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs",
"group": "arm-template"
},
{
"command": "azurerm-vscode-tools.sortParameters",
- "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters",
"group": "arm-template"
},
{
"command": "azurerm-vscode-tools.sortResources",
- "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == resources",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources",
"group": "arm-template"
},
{
"command": "azurerm-vscode-tools.sortVariables",
- "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == variables",
"group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertParameter",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters",
+ "group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertVariable",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == variables",
+ "group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertOutput",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs",
+ "group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertFunction",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == functions",
+ "group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertResource",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources",
+ "group": "arm-template"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortFunctions",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == functions",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortOutputs",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortParameters",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortResources",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortVariables",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == variables",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertParameter",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertVariable",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == variables",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertOutput",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertFunction",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == functions",
+ "group": "inline"
+ },
+ {
+ "command": "azurerm-vscode-tools.insertResource",
+ "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources",
+ "group": "inline"
}
],
"editor/title": [
@@ -357,6 +536,18 @@
"when": "azurerm-vscode-tools-isParamFile",
"$when.comment": "Shows up when it's a param file, but only enabled if there is an associated template file"
}
+ ],
+ "view/title": [
+ {
+ "command": "azurerm-vscode-tools.insertItem",
+ "when": "view == azurerm-vscode-tools.template-outline",
+ "group": "navigation@1"
+ },
+ {
+ "command": "azurerm-vscode-tools.sortTemplate",
+ "when": "view == azurerm-vscode-tools.template-outline",
+ "group": "navigation@2"
+ }
]
}
},
diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts
index 1714d5dd2..a60318d11 100644
--- a/src/AzureRMTools.ts
+++ b/src/AzureRMTools.ts
@@ -18,6 +18,7 @@ import { ext } from "./extensionVariables";
import { Histogram } from "./Histogram";
import * as Hover from './Hover';
import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue";
+import { getItemTypeQuickPicks, InsertItem } from "./insertItem";
import * as Json from "./JSON";
import * as language from "./Language";
import { startArmLanguageServer } from "./languageclient/startArmLanguageServer";
@@ -31,11 +32,12 @@ import { RenameCodeActionProvider } from "./RenameCodeActionProvider";
import { resetGlobalState } from "./resetGlobalState";
import { getPreferredSchema } from "./schemas";
import { getFunctionParamUsage } from "./signatureFormatting";
-import { getQuickPickItems, sortTemplate, SortType } from "./sortTemplate";
+import { getQuickPickItems, sortTemplate } from "./sortTemplate";
import { Stopwatch } from "./Stopwatch";
import { mightBeDeploymentParameters, mightBeDeploymentTemplate, templateDocumentSelector, templateOrParameterDocumentSelector } from "./supported";
import { survey } from "./survey";
import { TemplatePositionContext } from "./TemplatePositionContext";
+import { TemplateSectionType } from "./TemplateSectionType";
import * as TLE from "./TLE";
import { JsonOutlineProvider } from "./Treeview";
import { UnrecognizedBuiltinFunctionIssue } from "./UnrecognizedFunctionIssues";
@@ -104,6 +106,7 @@ export class AzureRMTools {
}
});
+ // tslint:disable-next-line:max-func-body-length
constructor(context: vscode.ExtensionContext) {
const jsonOutline: JsonOutlineProvider = new JsonOutlineProvider(context);
ext.jsonOutlineProvider = jsonOutline;
@@ -122,27 +125,27 @@ export class AzureRMTools {
uri = vscode.window.activeTextEditor?.document.uri;
}
if (uri && editor) {
- const sortType = await ext.ui.showQuickPick(getQuickPickItems(), { placeHolder: 'What do you want to sort?' });
- await this.sortTemplate(sortType.value, uri, editor);
+ const sectionType = await ext.ui.showQuickPick(getQuickPickItems(), { placeHolder: 'What do you want to sort?' });
+ await this.sortTemplate(sectionType.value, uri, editor);
}
});
registerCommand("azurerm-vscode-tools.sortFunctions", async () => {
- await this.sortTemplate(SortType.Functions);
+ await this.sortTemplate(TemplateSectionType.Functions);
});
registerCommand("azurerm-vscode-tools.sortOutputs", async () => {
- await this.sortTemplate(SortType.Outputs);
+ await this.sortTemplate(TemplateSectionType.Outputs);
});
registerCommand("azurerm-vscode-tools.sortParameters", async () => {
- await this.sortTemplate(SortType.Parameters);
+ await this.sortTemplate(TemplateSectionType.Parameters);
});
registerCommand("azurerm-vscode-tools.sortResources", async () => {
- await this.sortTemplate(SortType.Resources);
+ await this.sortTemplate(TemplateSectionType.Resources);
});
registerCommand("azurerm-vscode-tools.sortVariables", async () => {
- await this.sortTemplate(SortType.Variables);
+ await this.sortTemplate(TemplateSectionType.Variables);
});
registerCommand("azurerm-vscode-tools.sortTopLevel", async () => {
- await this.sortTemplate(SortType.TopLevel);
+ await this.sortTemplate(TemplateSectionType.TopLevel);
});
registerCommand(
"azurerm-vscode-tools.selectParameterFile", async (actionContext: IActionContext, source?: vscode.Uri) => {
@@ -158,6 +161,33 @@ export class AzureRMTools {
source = source ?? vscode.window.activeTextEditor?.document.uri;
await openTemplateFile(this._mapping, source, undefined);
});
+ registerCommand("azurerm-vscode-tools.insertItem", async (actionContext: IActionContext, uri?: vscode.Uri, editor?: vscode.TextEditor) => {
+ editor = editor || vscode.window.activeTextEditor;
+ uri = uri || vscode.window.activeTextEditor?.document.uri;
+ // If "Sort template..." was called from the context menu for ARM template outline
+ if (typeof uri === "string") {
+ uri = vscode.window.activeTextEditor?.document.uri;
+ }
+ if (uri && editor) {
+ const sectionType = await ext.ui.showQuickPick(getItemTypeQuickPicks(), { placeHolder: 'What do you want to insert?' });
+ await this.insertItem(sectionType.value, actionContext, uri, editor);
+ }
+ });
+ registerCommand("azurerm-vscode-tools.insertParameter", async (actionContext: IActionContext) => {
+ await this.insertItem(TemplateSectionType.Parameters, actionContext);
+ });
+ registerCommand("azurerm-vscode-tools.insertVariable", async (actionContext: IActionContext) => {
+ await this.insertItem(TemplateSectionType.Variables, actionContext);
+ });
+ registerCommand("azurerm-vscode-tools.insertOutput", async (actionContext: IActionContext) => {
+ await this.insertItem(TemplateSectionType.Outputs, actionContext);
+ });
+ registerCommand("azurerm-vscode-tools.insertFunction", async (actionContext: IActionContext) => {
+ await this.insertItem(TemplateSectionType.Functions, actionContext);
+ });
+ registerCommand("azurerm-vscode-tools.insertResource", async (actionContext: IActionContext) => {
+ await this.insertItem(TemplateSectionType.Resources, actionContext);
+ });
registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState);
registerCommand("azurerm-vscode-tools.codeAction.addAllMissingParameters", async (actionContext: IActionContext, source?: vscode.Uri) => {
await this.addMissingParameters(actionContext, source, false);
@@ -217,12 +247,21 @@ export class AzureRMTools {
}
}
- private async sortTemplate(sortType: SortType, documentUri?: vscode.Uri, editor?: vscode.TextEditor): Promise {
+ private async sortTemplate(sectionType: TemplateSectionType, documentUri?: vscode.Uri, editor?: vscode.TextEditor): Promise {
+ editor = editor || vscode.window.activeTextEditor;
+ documentUri = documentUri || editor?.document.uri;
+ if (editor && documentUri && editor.document.uri.fsPath === documentUri.fsPath) {
+ let deploymentTemplate = this.getOpenedDeploymentTemplate(editor.document);
+ await sortTemplate(deploymentTemplate, sectionType, editor);
+ }
+ }
+
+ private async insertItem(sectionType: TemplateSectionType, context: IActionContext, documentUri?: vscode.Uri, editor?: vscode.TextEditor): Promise {
editor = editor || vscode.window.activeTextEditor;
documentUri = documentUri || editor?.document.uri;
if (editor && documentUri && editor.document.uri.fsPath === documentUri.fsPath) {
let deploymentTemplate = this.getOpenedDeploymentTemplate(editor.document);
- await sortTemplate(deploymentTemplate, sortType, editor);
+ await new InsertItem(ext.ui).insertItem(deploymentTemplate, sectionType, editor, context);
}
}
diff --git a/src/TemplateSectionType.ts b/src/TemplateSectionType.ts
new file mode 100644
index 000000000..61f45daec
--- /dev/null
+++ b/src/TemplateSectionType.ts
@@ -0,0 +1,15 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ----------------------------------------------------------------------------
+
+/**
+ * The different sections of an ARM template
+ */
+export enum TemplateSectionType {
+ Resources,
+ Outputs,
+ Parameters,
+ Variables,
+ Functions,
+ TopLevel
+}
diff --git a/src/Treeview.ts b/src/Treeview.ts
index ca731e04b..26440ea8e 100644
--- a/src/Treeview.ts
+++ b/src/Treeview.ts
@@ -297,11 +297,10 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider {
* menu, as viewItem ==
*/
private getContextValue(elementInfo: IElementInfo): string | undefined {
- if (elementInfo.current.level === 1) {
- const keyNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start, Contains.strict);
- if (keyNode instanceof Json.StringValue) {
- return keyNode.unquotedValue;
- }
+ let element = elementInfo.current.level === 1 ? elementInfo.current : elementInfo.root;
+ const keyNode = this.tree && this.tree.getValueAtCharacterIndex(element.key.start, Contains.strict);
+ if (keyNode instanceof Json.StringValue) {
+ return keyNode.unquotedValue;
}
return undefined;
}
diff --git a/src/insertItem.ts b/src/insertItem.ts
new file mode 100644
index 000000000..62dc09fd2
--- /dev/null
+++ b/src/insertItem.ts
@@ -0,0 +1,453 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as assert from "assert";
+import * as fse from 'fs-extra';
+import * as path from "path";
+import * as vscode from "vscode";
+// tslint:disable-next-line:no-duplicate-imports
+import { commands } from "vscode";
+import { IActionContext, IAzureUserInput } from "vscode-azureextensionui";
+import { Json, templateKeys } from "../extension.bundle";
+import { assetsPath } from "./constants";
+import { DeploymentTemplate } from "./DeploymentTemplate";
+import { ext } from './extensionVariables';
+import { TemplateSectionType } from "./TemplateSectionType";
+import { assertNever } from './util/assertNever';
+
+const insertCursorText = '[]';
+
+export class QuickPickItem implements vscode.QuickPickItem {
+ public label: string;
+ public value: T;
+ public description: string;
+
+ constructor(label: string, value: T, description: string) {
+ this.label = label;
+ this.value = value;
+ this.description = description;
+ }
+}
+
+export function getItemType(): QuickPickItem[] {
+ let items: QuickPickItem[] = [];
+ items.push(new QuickPickItem("String", "string", "A string"));
+ items.push(new QuickPickItem("Secure string", "securestring", "A secure string"));
+ items.push(new QuickPickItem("Int", "int", "An integer"));
+ items.push(new QuickPickItem("Bool", "bool", "A boolean"));
+ items.push(new QuickPickItem("Object", "object", "An object"));
+ items.push(new QuickPickItem("Secure object", "secureobject", "A secure object"));
+ items.push(new QuickPickItem("Array", "array", "An array"));
+ return items;
+}
+
+function getResourceSnippets(): vscode.QuickPickItem[] {
+ let items: vscode.QuickPickItem[] = [];
+ let snippetPath = path.join(assetsPath, "armsnippets.jsonc");
+ let content = fse.readFileSync(snippetPath, "utf8");
+ let tree = Json.parse(content);
+ if (!(tree.value instanceof Json.ObjectValue)) {
+ return items;
+ }
+ for (const property of tree.value.properties) {
+ if (isResourceSnippet(property)) {
+ items.push(getQuickPickItem(property.nameValue.unquotedValue));
+ }
+ }
+ return items.sort((a, b) => a.label.localeCompare(b.label));
+}
+
+function isResourceSnippet(snippet: Json.Property): boolean {
+ if (!snippet.value || !(snippet.value instanceof Json.ObjectValue)) {
+ return false;
+ }
+ let body = snippet.value.getProperty("body");
+ if (!body || !(body.value instanceof Json.ArrayValue)) {
+ return false;
+ }
+ for (const row of body.value.elements) {
+ if (!(row instanceof Json.StringValue)) {
+ continue;
+ }
+ if (row.unquotedValue.indexOf("\"Microsoft.") >= 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function getQuickPickItem(label: string): vscode.QuickPickItem {
+ return { label: label };
+}
+
+export function getItemTypeQuickPicks(): QuickPickItem[] {
+ let items: QuickPickItem[] = [];
+ items.push(new QuickPickItem("Function", TemplateSectionType.Functions, "Insert a function"));
+ items.push(new QuickPickItem("Output", TemplateSectionType.Outputs, "Insert an output"));
+ items.push(new QuickPickItem("Parameter", TemplateSectionType.Parameters, "Insert a parameter"));
+ items.push(new QuickPickItem("Resource", TemplateSectionType.Resources, "Insert a resource"));
+ items.push(new QuickPickItem("Variable", TemplateSectionType.Variables, "Insert a variable"));
+ return items;
+}
+
+export class InsertItem {
+ private ui: IAzureUserInput;
+
+ constructor(ui: IAzureUserInput) {
+ this.ui = ui;
+ }
+
+ public async insertItem(template: DeploymentTemplate | undefined, sectionType: TemplateSectionType, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ if (!template) {
+ return;
+ }
+ ext.outputChannel.appendLine("Insert item");
+ switch (sectionType) {
+ case TemplateSectionType.Functions:
+ await this.insertFunction(template, textEditor, context);
+ vscode.window.showInformationMessage("Please type the output of the function.");
+ break;
+ case TemplateSectionType.Outputs:
+ await this.insertOutput(template, textEditor, context);
+ vscode.window.showInformationMessage("Please type the the value of the output.");
+ break;
+ case TemplateSectionType.Parameters:
+ await this.insertParameter(template, textEditor, context);
+ vscode.window.showInformationMessage("Done inserting parameter.");
+ break;
+ case TemplateSectionType.Resources:
+ await this.insertResource(template, textEditor, context);
+ vscode.window.showInformationMessage("Press TAB to move between the tab stops.");
+ break;
+ case TemplateSectionType.Variables:
+ await this.insertVariable(template, textEditor, context);
+ vscode.window.showInformationMessage("Please type the the value of the variable.");
+ break;
+ case TemplateSectionType.TopLevel:
+ assert.fail("Unknown insert item type!");
+ default:
+ assertNever(sectionType);
+ }
+ }
+
+ private getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined {
+ return this.getTemplatePart(template, templatePart)?.asObjectValue;
+ }
+
+ private getTemplateArrayPart(template: DeploymentTemplate, templatePart: string): Json.ArrayValue | undefined {
+ return this.getTemplatePart(template, templatePart)?.asArrayValue;
+ }
+
+ private getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.Value | undefined {
+ return template.topLevelValue?.getPropertyValue(templatePart);
+ }
+
+ private async insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ let name = await this.ui.showInputBox({ prompt: "Name of parameter?" });
+ const parameterType = await this.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' });
+ let parameter: Parameter = {
+ type: parameterType.value
+ };
+ let defaultValue = await this.ui.showInputBox({ prompt: "Default value? Leave empty for no default value.", });
+ if (defaultValue) {
+ parameter.defaultValue = defaultValue;
+ }
+ let description = await this.ui.showInputBox({ prompt: "Description? Leave empty for no description.", });
+ if (description) {
+ parameter.metadata = {
+ description: description
+ };
+ }
+ await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name, context);
+ }
+
+ private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: Data | unknown, name: string, context: IActionContext): Promise {
+ let templatePart = this.getTemplateObjectPart(template, part);
+ if (!templatePart) {
+ let topLevel = template.topLevelValue;
+ if (!topLevel) {
+ context.errorHandling.suppressReportIssue = true;
+ throw new Error("Invalid ARM template!");
+ }
+ let subPart: Data = {};
+ subPart[name] = data;
+ await this.insertInObjectHelper(topLevel, textEditor, subPart, part, 1);
+ } else {
+ await this.insertInObjectHelper(templatePart, textEditor, data, name);
+ }
+ }
+
+ /**
+ * Insert data into an template object (parameters, variables and outputs).
+ * @param templatePart The template part to insert into.
+ * @param textEditor The text editor to insert into.
+ * @param data The data to insert.
+ * @param name The name (key) to be inserted.
+ * @param indentLevel Which indent level to use when inserting.
+ * @returns The document index of the cursor after the text has been inserted.
+ */
+ // tslint:disable-next-line:no-any
+ private async insertInObjectHelper(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise {
+ let isFirstItem = templatePart.properties.length === 0;
+ let startText = isFirstItem ? '' : ',';
+ let index = isFirstItem ? templatePart.span.endIndex :
+ templatePart.properties[templatePart.properties.length - 1].span.afterEndIndex;
+ let tabs = '\t'.repeat(indentLevel - 1);
+ let endText = isFirstItem ? `\r\n${tabs}` : ``;
+ let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : `"${data}"`;
+ let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel);
+ return await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`);
+ }
+
+ private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ let name = await this.ui.showInputBox({ prompt: "Name of variable?" });
+ await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name, context);
+ }
+
+ private async insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ let name = await this.ui.showInputBox({ prompt: "Name of output?" });
+ const outputType = await this.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' });
+ let output: Output = {
+ type: outputType.value,
+ value: insertCursorText.replace(/"/g, '')
+ };
+ await this.insertInObject(template, textEditor, templateKeys.outputs, output, name, context);
+ }
+ private async insertFunctionAsTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ if (!topLevel) {
+ context.errorHandling.suppressReportIssue = true;
+ throw new Error("Invalid ARM template!");
+ }
+ let functions = [await this.getFunctionNamespace()];
+ await this.insertInObjectHelper(topLevel, textEditor, functions, "functions", 1);
+ }
+
+ private async insertFunctionAsNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise {
+ let namespace = await this.getFunctionNamespace();
+ await this.insertInArray(functions, textEditor, namespace);
+ }
+
+ private async insertFunctionAsMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise {
+ let functionName = await this.ui.showInputBox({ prompt: "Name of function?" });
+ let functionDef = await this.getFunction();
+ // tslint:disable-next-line:no-any
+ let members: any = {};
+ // tslint:disable-next-line:no-unsafe-any
+ members[functionName] = functionDef;
+ await this.insertInObjectHelper(namespace, textEditor, members, 'members', 3);
+ }
+
+ private async insertFunctionAsFunction(members: Json.ObjectValue, textEditor: vscode.TextEditor): Promise {
+ let functionName = await this.ui.showInputBox({ prompt: "Name of function?" });
+ let functionDef = await this.getFunction();
+ await this.insertInObjectHelper(members, textEditor, functionDef, functionName, 4);
+ }
+
+ private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ let functions = this.getTemplateArrayPart(template, templateKeys.functions);
+ if (!functions) {
+ // tslint:disable-next-line:no-unsafe-any
+ await this.insertFunctionAsTopLevel(template.topLevelValue, textEditor, context);
+ return;
+ }
+ if (functions.length === 0) {
+ await this.insertFunctionAsNamespace(functions, textEditor);
+ return;
+ }
+ let namespace = Json.asObjectValue(functions.elements[0]);
+ if (!namespace) {
+ context.errorHandling.suppressReportIssue = true;
+ throw new Error("The first namespace in functions is not an object!");
+ }
+ let members = namespace.getPropertyValue("members");
+ if (!members) {
+ await this.insertFunctionAsMembers(namespace, textEditor);
+ return;
+ }
+ let membersObject = Json.asObjectValue(members);
+ if (!membersObject) {
+ context.errorHandling.suppressReportIssue = true;
+ throw new Error("The first namespace in functions does not have members as an object!");
+ }
+ await this.insertFunctionAsFunction(membersObject, textEditor);
+ return;
+ }
+
+ private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise {
+ let resources = this.getTemplateArrayPart(template, templateKeys.resources);
+ let index: number;
+ let prepend = "\r\n\t\t\r\n\t";
+ if (!resources) {
+ if (!template.topLevelValue) {
+ context.errorHandling.suppressReportIssue = true;
+ throw new Error("Invalid ARM template!");
+ }
+ // tslint:disable-next-line:no-any
+ let subPart: any = [];
+ index = await this.insertInObjectHelper(template.topLevelValue, textEditor, subPart, templateKeys.resources, 1);
+ } else {
+ index = resources.span.endIndex;
+ if (resources.elements.length > 0) {
+ let lastIndex = resources.elements.length - 1;
+ index = resources.elements[lastIndex].span.afterEndIndex;
+ prepend = `,\r\n\t\t`;
+ }
+ }
+ const resource = await this.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' });
+ await this.insertText(textEditor, index, prepend);
+ let newCursorPosition = this.getCursorPositionForInsertResource(textEditor, index, prepend);
+ textEditor.selection = new vscode.Selection(newCursorPosition, newCursorPosition);
+ await commands.executeCommand('editor.action.insertSnippet', { name: resource.label });
+ textEditor.revealRange(new vscode.Range(newCursorPosition, newCursorPosition), vscode.TextEditorRevealType.Default);
+ }
+
+ private getCursorPositionForInsertResource(textEditor: vscode.TextEditor, index: number, prepend: string): vscode.Position {
+ let prependRange = new vscode.Range(textEditor.document.positionAt(index), textEditor.document.positionAt(index + this.formatText(prepend, textEditor).length));
+ let prependFromDocument = textEditor.document.getText(prependRange);
+ let lookFor = this.formatText('\t\t', textEditor);
+ let cursorPos = prependFromDocument.indexOf(lookFor);
+ return textEditor.document.positionAt(index + cursorPos + lookFor.length);
+ }
+
+ private async getFunction(): Promise {
+ const outputType = await this.ui.showQuickPick(getItemType(), { placeHolder: 'Type of function output?' });
+ let parameters = await this.getFunctionParameters();
+ let functionDef = {
+ parameters: parameters,
+ output: {
+ type: outputType.value,
+ value: insertCursorText
+ }
+ };
+ return functionDef;
+ }
+
+ private async getFunctionParameters(): Promise {
+ let parameterName: string;
+ let parameters = [];
+ do {
+ parameterName = await this.ui.showInputBox({ prompt: "Name of parameter? Leave empty for no more parameters" });
+ if (parameterName !== '') {
+ const parameterType = await this.ui.showQuickPick(getItemType(), { placeHolder: `Type of parameter ${parameterName}?` });
+ parameters.push({
+ name: parameterName,
+ type: parameterType.value
+ });
+ }
+
+ } while (parameterName !== '');
+ return parameters;
+ }
+
+ // tslint:disable-next-line:no-any
+ private async insertInArray(templatePart: Json.ArrayValue, textEditor: vscode.TextEditor, data: any): Promise {
+ let index = templatePart.span.endIndex;
+ let text = JSON.stringify(data, null, '\t');
+ let indentedText = this.indent(`\r\n${text}\r\n`, 2);
+ await this.insertText(textEditor, index, `${indentedText}\t`);
+ }
+
+ private async getFunctionNamespace(): Promise {
+ let namespaceName = await this.ui.showInputBox({ prompt: "Name of namespace?" });
+ let namespace = {
+ namespace: namespaceName,
+ members: await this.getFunctionMembers()
+ };
+ return namespace;
+ }
+
+ // tslint:disable-next-line:no-any
+ private async getFunctionMembers(): Promise {
+ let functionName = await this.ui.showInputBox({ prompt: "Name of function?" });
+ let functionDef = await this.getFunction();
+ // tslint:disable-next-line:no-any
+ let members: any = {};
+ // tslint:disable-next-line:no-unsafe-any
+ members[functionName] = functionDef;
+ return members;
+ }
+
+ /**
+ * Insert text into the document.
+ * @param textEditor The text editor to insert the text into.
+ * @param index The document index where to insert the text.
+ * @param text The text to be inserted.
+ * @returns The document index of the cursor after the text has been inserted.
+ */
+ private async insertText(textEditor: vscode.TextEditor, index: number, text: string): Promise {
+ text = this.formatText(text, textEditor);
+ let pos = textEditor.document.positionAt(index);
+ await textEditor.edit(builder => builder.insert(pos, text));
+ textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default);
+ if (text.lastIndexOf(insertCursorText) >= 0) {
+ let insertedText = textEditor.document.getText(new vscode.Range(pos, textEditor.document.positionAt(index + text.length)));
+ let cursorPos = insertedText.lastIndexOf(insertCursorText);
+ let newIndex = index + cursorPos + insertCursorText.length / 2;
+ let pos2 = textEditor.document.positionAt(newIndex);
+ textEditor.selection = new vscode.Selection(pos2, pos2);
+ return newIndex;
+ }
+ return 0;
+ }
+
+ private formatText(text: string, textEditor: vscode.TextEditor): string {
+ if (textEditor.options.insertSpaces === true) {
+ text = text.replace(/\t/g, ' '.repeat(Number(textEditor.options.tabSize)));
+ } else {
+ text = text.replace(/ {4}/g, '\t');
+ }
+ return text;
+ }
+
+ /**
+ * Indents the given string
+ * @param str The string to be indented.
+ * @param numOfTabs The amount of indentations to place at the
+ * beginning of each line of the string.
+ * @return The new string with each line beginning with the desired
+ * amount of indentation.
+ */
+ private indent(str: string, numOfTabs: number): string {
+ // tslint:disable-next-line:prefer-array-literal
+ str = str.replace(/^(?=.)/gm, '\t'.repeat(numOfTabs));
+ return str;
+ }
+}
+
+interface ParameterMetaData {
+ description: string;
+}
+
+interface Parameter extends Data {
+ // tslint:disable-next-line:no-reserved-keywords
+ type: string;
+ defaultValue?: string;
+ metadata?: ParameterMetaData;
+}
+
+interface Output extends Data {
+ // tslint:disable-next-line:no-reserved-keywords
+ type: string;
+ value: string;
+}
+
+interface Function extends Data {
+ parameters: Parameter[];
+ output: Output;
+}
+
+interface FunctionParameter extends Data {
+ name: string;
+ // tslint:disable-next-line:no-reserved-keywords
+ type: string;
+}
+
+interface FunctionNameSpace {
+ namespace: string;
+ // tslint:disable-next-line:no-any
+ members: any[];
+}
+
+type Data = { [key: string]: unknown };
diff --git a/src/sortTemplate.ts b/src/sortTemplate.ts
index ea9a3e6dd..63313b6d3 100644
--- a/src/sortTemplate.ts
+++ b/src/sortTemplate.ts
@@ -11,28 +11,21 @@ import { ext } from './extensionVariables';
import { IParameterDefinition } from './IParameterDefinition';
import * as Json from "./JSON";
import * as language from "./Language";
+import { TemplateSectionType } from "./TemplateSectionType";
import { UserFunctionDefinition } from './UserFunctionDefinition';
import { UserFunctionNamespaceDefinition } from './UserFunctionNamespaceDefinition';
+import { assertNever } from "./util/assertNever";
import { IVariableDefinition } from './VariableDefinition';
-export enum SortType {
- Resources,
- Outputs,
- Parameters,
- Variables,
- Functions,
- TopLevel
-}
-
// A map of [token starting index] to [span of all comments before that token]
type CommentsMap = Map;
export class SortQuickPickItem implements vscode.QuickPickItem {
public label: string;
- public value: SortType;
+ public value: TemplateSectionType;
public description: string;
- constructor(label: string, value: SortType, description: string) {
+ constructor(label: string, value: TemplateSectionType, description: string) {
this.label = label;
this.value = value;
this.description = description;
@@ -41,42 +34,41 @@ export class SortQuickPickItem implements vscode.QuickPickItem {
export function getQuickPickItems(): SortQuickPickItem[] {
let items: SortQuickPickItem[] = [];
- items.push(new SortQuickPickItem("Functions", SortType.Functions, "Sort function namespaces and functions"));
- items.push(new SortQuickPickItem("Outputs", SortType.Outputs, "Sort outputs"));
- items.push(new SortQuickPickItem("Parameters", SortType.Parameters, "Sort parameters for the template"));
- items.push(new SortQuickPickItem("Resources", SortType.Resources, "Sort resources based on the name including first level of child resources"));
- items.push(new SortQuickPickItem("Variables", SortType.Variables, "Sort variables"));
- items.push(new SortQuickPickItem("Top level", SortType.TopLevel, "Sort top level items based on recommended order (parameters, functions, variables, resources, outputs)"));
+ items.push(new SortQuickPickItem("Functions", TemplateSectionType.Functions, "Sort function namespaces and functions"));
+ items.push(new SortQuickPickItem("Outputs", TemplateSectionType.Outputs, "Sort outputs"));
+ items.push(new SortQuickPickItem("Parameters", TemplateSectionType.Parameters, "Sort parameters for the template"));
+ items.push(new SortQuickPickItem("Resources", TemplateSectionType.Resources, "Sort resources based on the name including first level of child resources"));
+ items.push(new SortQuickPickItem("Variables", TemplateSectionType.Variables, "Sort variables"));
+ items.push(new SortQuickPickItem("Top level", TemplateSectionType.TopLevel, "Sort top level items based on recommended order (parameters, functions, variables, resources, outputs)"));
return items;
}
-export async function sortTemplate(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise {
+export async function sortTemplate(template: DeploymentTemplate | undefined, sectionType: TemplateSectionType, textEditor: vscode.TextEditor): Promise {
if (!template) {
return;
}
ext.outputChannel.appendLine("Sorting template");
- switch (sortType) {
- case SortType.Functions:
+ switch (sectionType) {
+ case TemplateSectionType.Functions:
await sortFunctions(template, textEditor);
break;
- case SortType.Outputs:
+ case TemplateSectionType.Outputs:
await sortOutputs(template, textEditor);
break;
- case SortType.Parameters:
+ case TemplateSectionType.Parameters:
await sortParameters(template, textEditor);
break;
- case SortType.Resources:
+ case TemplateSectionType.Resources:
await sortResources(template, textEditor);
break;
- case SortType.Variables:
+ case TemplateSectionType.Variables:
await sortVariables(template, textEditor);
break;
- case SortType.TopLevel:
+ case TemplateSectionType.TopLevel:
await sortTopLevel(template, textEditor);
break;
default:
- vscode.window.showWarningMessage("Unknown sort type!");
- return;
+ assertNever(sectionType);
}
vscode.window.showInformationMessage("Done sorting template!");
diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts
new file mode 100644
index 000000000..d84a54990
--- /dev/null
+++ b/test/functional/insertItem.test.ts
@@ -0,0 +1,473 @@
+// ----------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ----------------------------------------------------------------------------
+
+// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length no-http-string no-suspicious-comment
+// tslint:disable:no-non-null-assertion
+
+// WARNING: At the breakpoint, the extension will be in an inactivate state (i.e., if you make changes in the editor, diagnostics,
+// formatting, etc. will not be updated until you F5 again)
+import * as assert from 'assert';
+import * as fse from 'fs-extra';
+import * as vscode from "vscode";
+// tslint:disable-next-line:no-duplicate-imports
+import { window, workspace } from "vscode";
+import { IActionContext, IAzureUserInput } from 'vscode-azureextensionui';
+import { DeploymentTemplate, InsertItem, TemplateSectionType } from '../../extension.bundle';
+import { getTempFilePath } from "../support/getTempFilePath";
+
+suite("InsertItem", async (): Promise => {
+ function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor, ignoreWhiteSpace: boolean = false): void {
+ if (textEditor.options.insertSpaces === true) {
+ expected = expected.replace(/ {4}/g, ' '.repeat(Number(textEditor.options.tabSize)));
+ if (ignoreWhiteSpace) {
+ expected = expected.replace(/ +/g, ' ');
+ actual = actual.replace(/ +/g, ' ');
+ }
+ } else {
+ expected = expected.replace(/ {4}/g, '\t');
+ if (ignoreWhiteSpace) {
+ expected = expected.replace(/\t+/g, '\t');
+ actual = actual.replace(/\t+/g, '\t');
+ }
+ }
+ if (textEditor.document.eol === vscode.EndOfLine.CRLF) {
+ expected = expected.replace(/\n/g, '\r\n');
+ }
+ assert.equal(actual, expected);
+ }
+
+ async function testInsertItem(template: string, expected: String, action: (insertItem: InsertItem, deploymentTemplate: DeploymentTemplate, textEditor: vscode.TextEditor) => Promise, showInputBox: string[], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise {
+ test("Tabs CRLF", async () => {
+ await testInsertItemWithSettings(template, expected, false, 4, true, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ test("Spaces CRLF", async () => {
+ await testInsertItemWithSettings(template, expected, true, 4, true, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ test("Spaces (2) CRLF", async () => {
+ await testInsertItemWithSettings(template, expected, true, 2, true, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ test("Spaces LF", async () => {
+ await testInsertItemWithSettings(template, expected, true, 4, false, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ test("Tabs LF", async () => {
+ await testInsertItemWithSettings(template, expected, false, 4, false, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ test("Spaces (2) LF", async () => {
+ await testInsertItemWithSettings(template, expected, true, 2, false, action, showInputBox, textToInsert, ignoreWhiteSpace);
+ });
+ }
+
+ async function testInsertItemWithSettings(template: string, expected: String, insertSpaces: boolean, tabSize: number, eolAsCRLF: boolean, action: (insertItem: InsertItem, deploymentTemplate: DeploymentTemplate, textEditor: vscode.TextEditor) => Promise, showInputBox: string[], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise {
+ if (eolAsCRLF) {
+ template = template.replace(/\n/g, '\r\n');
+ }
+ if (insertSpaces && tabSize !== 4) {
+ template = template.replace(/ {4}/g, ' '.repeat(tabSize));
+ }
+ if (!insertSpaces) {
+ template = template.replace(/ {4}/g, '\t');
+ }
+ const tempPath = getTempFilePath(`insertItem`, '.azrm');
+ fse.writeFileSync(tempPath, template);
+ let document = await workspace.openTextDocument(tempPath);
+ let textEditor = await window.showTextDocument(document);
+ let ui = new MockUserInput(showInputBox);
+ let insertItem = new InsertItem(ui);
+ let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri);
+ await action(insertItem, deploymentTemplate, textEditor);
+ await textEditor.edit(builder => builder.insert(textEditor.selection.active, textToInsert));
+ const docTextAfterInsertion = document.getText();
+ assertTemplate(docTextAfterInsertion, expected, textEditor, ignoreWhiteSpace);
+ }
+
+ const totallyEmptyTemplate =
+ `{}`;
+
+ async function doTestInsertItem(startTemplate: string, expectedTemplate: string, sectionType: TemplateSectionType, showInputBox: string[] = [], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise {
+ await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, sectionType, editor, getActionContext()), showInputBox, textToInsert, ignoreWhiteSpace);
+ }
+
+ suite("Variables", async () => {
+ const emptyTemplate =
+ `{
+ "variables": {}
+}`;
+ const oneVariableTemplate = `{
+ "variables": {
+ "variable1": "[resourceGroup()]"
+ }
+}`;
+ const twoVariablesTemplate = `{
+ "variables": {
+ "variable1": "[resourceGroup()]",
+ "variable2": "[resourceGroup()]"
+ }
+}`;
+ const threeVariablesTemplate = `{
+ "variables": {
+ "variable1": "[resourceGroup()]",
+ "variable2": "[resourceGroup()]",
+ "variable3": "[resourceGroup()]"
+ }
+}`;
+ suite("Insert one variable", async () => {
+ await doTestInsertItem(emptyTemplate, oneVariableTemplate, TemplateSectionType.Variables, ["variable1"], 'resourceGroup()');
+ });
+ suite("Insert one more variable", async () => {
+ await doTestInsertItem(oneVariableTemplate, twoVariablesTemplate, TemplateSectionType.Variables, ["variable2"], 'resourceGroup()');
+ });
+ suite("Insert even one more variable", async () => {
+ await doTestInsertItem(twoVariablesTemplate, threeVariablesTemplate, TemplateSectionType.Variables, ["variable3"], 'resourceGroup()');
+ });
+
+ suite("Insert one variable in totally empty template", async () => {
+ await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, TemplateSectionType.Variables, ["variable1"], 'resourceGroup()');
+ });
+ });
+
+ suite("Resources", async () => {
+ const emptyTemplate =
+ `{
+ "resources": []
+}`;
+ const oneResourceTemplate = `{
+ "resources": [
+ {
+ "name": "keyVault1/keyVaultSecret1",
+ "type": "Microsoft.KeyVault/vaults/secrets",
+ "apiVersion": "2016-10-01",
+ "properties": {
+ "value": "secretValue"
+ }
+ }
+ ]
+}`;
+ const twoResourcesTemplate = `{
+ "resources": [
+ {
+ "name": "keyVault1/keyVaultSecret1",
+ "type": "Microsoft.KeyVault/vaults/secrets",
+ "apiVersion": "2016-10-01",
+ "properties": {
+ "value": "secretValue"
+ }
+ },
+ {
+ "name": "applicationSecurityGroup1",
+ "type": "Microsoft.Network/applicationSecurityGroups",
+ "apiVersion": "2019-11-01",
+ "location": "[resourceGroup().location]",
+ "tags": {
+ },
+ "properties": {
+ }
+ }
+ ]
+}`;
+
+ suite("Insert one resource (KeyVault Secret) into totally empty template", async () => {
+ await doTestInsertItem(totallyEmptyTemplate, oneResourceTemplate, TemplateSectionType.Resources, ["KeyVault Secret"], '', true);
+ });
+ suite("Insert one resource (KeyVault Secret)", async () => {
+ await doTestInsertItem(emptyTemplate, oneResourceTemplate, TemplateSectionType.Resources, ["KeyVault Secret"], '', true);
+ });
+ suite("Insert one more resource (Application Security Group)", async () => {
+ await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, TemplateSectionType.Resources, ["Application Security Group"], '', true);
+ });
+ });
+
+ suite("Functions", async () => {
+ const emptyTemplate =
+ `{
+ "functions": []
+}`;
+ const namespaceTemplate = `{
+ "functions": [
+ {
+ "namespace": "ns"
+ }
+ ]
+}`;
+ const membersTemplate = `{
+ "functions": [
+ {
+ "namespace": "ns",
+ "members": {}
+ }
+ ]
+}`;
+ const oneFunctionTemplate = `{
+ "functions": [
+ {
+ "namespace": "ns",
+ "members": {
+ "function1": {
+ "parameters": [
+ {
+ "name": "parameter1",
+ "type": "string"
+ }
+ ],
+ "output": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ }
+ }
+ }
+ ]
+}`;
+ const twoFunctionsTemplate = `{
+ "functions": [
+ {
+ "namespace": "ns",
+ "members": {
+ "function1": {
+ "parameters": [
+ {
+ "name": "parameter1",
+ "type": "string"
+ }
+ ],
+ "output": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ },
+ "function2": {
+ "parameters": [],
+ "output": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ }
+ }
+ }
+ ]
+}`;
+ const threeFunctionsTemplate = `{
+ "functions": [
+ {
+ "namespace": "ns",
+ "members": {
+ "function1": {
+ "parameters": [
+ {
+ "name": "parameter1",
+ "type": "string"
+ }
+ ],
+ "output": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ },
+ "function2": {
+ "parameters": [],
+ "output": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ },
+ "function3": {
+ "parameters": [
+ {
+ "name": "parameter1",
+ "type": "string"
+ },
+ {
+ "name": "parameter2",
+ "type": "bool"
+ }
+ ],
+ "output": {
+ "type": "securestring",
+ "value": "[resourceGroup()]"
+ }
+ }
+ }
+ }
+ ]
+}`;
+ suite("Insert function", async () => {
+ await doTestInsertItem(emptyTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()");
+ });
+ suite("Insert one more function", async () => {
+ await doTestInsertItem(oneFunctionTemplate, twoFunctionsTemplate, TemplateSectionType.Functions, ["function2", "String", ""], "resourceGroup()");
+ });
+ suite("Insert one function in totally empty template", async () => {
+ await doTestInsertItem(totallyEmptyTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()");
+ });
+ suite("Insert function in namespace", async () => {
+ await doTestInsertItem(namespaceTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()");
+ });
+ suite("Insert function in members", async () => {
+ await doTestInsertItem(membersTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()");
+ });
+ suite("Insert even one more function", async () => {
+ await doTestInsertItem(twoFunctionsTemplate, threeFunctionsTemplate, TemplateSectionType.Functions, ["function3", "Secure string", "parameter1", "String", "parameter2", "Bool", ""], "resourceGroup()");
+ });
+ });
+
+ suite("Parameters", async () => {
+ const emptyTemplate =
+ `{
+ "parameters": {}
+}`;
+ const oneParameterTemplate = `{
+ "parameters": {
+ "parameter1": {
+ "type": "string",
+ "defaultValue": "default",
+ "metadata": {
+ "description": "description"
+ }
+ }
+ }
+}`;
+ const twoParametersTemplate = `{
+ "parameters": {
+ "parameter1": {
+ "type": "string",
+ "defaultValue": "default",
+ "metadata": {
+ "description": "description"
+ }
+ },
+ "parameter2": {
+ "type": "string"
+ }
+ }
+}`;
+ const threeParametersTemplate = `{
+ "parameters": {
+ "parameter1": {
+ "type": "string",
+ "defaultValue": "default",
+ "metadata": {
+ "description": "description"
+ }
+ },
+ "parameter2": {
+ "type": "string"
+ },
+ "parameter3": {
+ "type": "securestring",
+ "metadata": {
+ "description": "description3"
+ }
+ }
+ }
+}`;
+ suite("Insert one parameter", async () => {
+ await doTestInsertItem(emptyTemplate, oneParameterTemplate, TemplateSectionType.Parameters, ["parameter1", "String", "default", "description"]);
+ });
+ suite("Insert one more parameter", async () => {
+ await doTestInsertItem(oneParameterTemplate, twoParametersTemplate, TemplateSectionType.Parameters, ["parameter2", "String", "", ""]);
+ });
+ suite("Insert even one more parameter", async () => {
+ await doTestInsertItem(twoParametersTemplate, threeParametersTemplate, TemplateSectionType.Parameters, ["parameter3", "Secure string", "", "description3"]);
+ });
+ suite("Insert one output in totally empty template", async () => {
+ await doTestInsertItem(totallyEmptyTemplate, oneParameterTemplate, TemplateSectionType.Parameters, ["parameter1", "String", "default", "description"]);
+ });
+ });
+
+ suite("Outputs", async () => {
+ const emptyTemplate =
+ `{
+ "outputs": {}
+}`;
+ const oneOutputTemplate = `{
+ "outputs": {
+ "output1": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ }
+}`;
+ const twoOutputsTemplate = `{
+ "outputs": {
+ "output1": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ },
+ "output2": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ }
+ }
+}`;
+ const threeOutputsTemplate = `{
+ "outputs": {
+ "output1": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ },
+ "output2": {
+ "type": "string",
+ "value": "[resourceGroup()]"
+ },
+ "output3": {
+ "type": "securestring",
+ "value": "[resourceGroup()]"
+ }
+ }
+}`;
+ suite("Insert one output", async () => {
+ await doTestInsertItem(emptyTemplate, oneOutputTemplate, TemplateSectionType.Outputs, ["output1", "String"], 'resourceGroup()');
+ });
+ suite("Insert one more output", async () => {
+ await doTestInsertItem(oneOutputTemplate, twoOutputsTemplate, TemplateSectionType.Outputs, ["output2", "String"], 'resourceGroup()');
+ });
+ suite("Insert even one more output", async () => {
+ await doTestInsertItem(twoOutputsTemplate, threeOutputsTemplate, TemplateSectionType.Outputs, ["output3", "Secure string"], 'resourceGroup()');
+ });
+ suite("Insert one output in totally empty template", async () => {
+ await doTestInsertItem(emptyTemplate, oneOutputTemplate, TemplateSectionType.Outputs, ["output1", "String"], 'resourceGroup()');
+ });
+ });
+});
+
+class MockUserInput implements IAzureUserInput {
+ private showInputBoxTexts: string[] = [];
+ constructor(showInputBox: string[]) {
+ this.showInputBoxTexts = Object.assign([], showInputBox);
+ }
+ public async showQuickPick(items: T[] | Thenable, options: import("vscode-azureextensionui").IAzureQuickPickOptions): Promise {
+ let result = await items;
+ let label = this.showInputBoxTexts.shift()!;
+ let item = result.find(x => x.label === label)!;
+ return item;
+ }
+
+ public async showInputBox(options: vscode.InputBoxOptions): Promise {
+ return this.showInputBoxTexts.shift()!;
+ }
+
+ public async showWarningMessage(message: string, options: import("vscode-azureextensionui").IAzureMessageOptions, ...items: T[]): Promise {
+ return items[0];
+ }
+
+ public async showOpenDialog(options: vscode.OpenDialogOptions): Promise {
+ return [vscode.Uri.file("c:\\some\\path")];
+ }
+}
+
+function getActionContext(): IActionContext {
+ return {
+ telemetry: {
+ measurements: {},
+ properties: {},
+ suppressAll: true,
+ suppressIfSuccessful: true
+ },
+ errorHandling: {
+ issueProperties: {},
+ rethrow: false,
+ suppressDisplay: true,
+ suppressReportIssue: true
+ }
+ };
+}