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 + } + }; +}