From e2c93e0fdbcddb4f4e9a93033148f89320415d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Fri, 13 Dec 2019 14:48:10 +0100 Subject: [PATCH 01/61] Added icons for 6 new resource types in JsonOutlineProvider --- icons/applicationinsights.svg | 9 ++++ icons/appservices.svg | 7 +++ icons/cdnprofiles.svg | 19 ++++++++ icons/keyvaults.svg | 9 ++++ icons/settings.svg | 83 +++++++++++++++++++++++++++++++++++ src/Treeview.ts | 9 +++- test/Treeview.test.ts | 38 ++++++++++++++++ 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 icons/applicationinsights.svg create mode 100644 icons/appservices.svg create mode 100644 icons/cdnprofiles.svg create mode 100644 icons/keyvaults.svg create mode 100644 icons/settings.svg diff --git a/icons/applicationinsights.svg b/icons/applicationinsights.svg new file mode 100644 index 000000000..1c247f84c --- /dev/null +++ b/icons/applicationinsights.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/appservices.svg b/icons/appservices.svg new file mode 100644 index 000000000..ebea5e956 --- /dev/null +++ b/icons/appservices.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/icons/cdnprofiles.svg b/icons/cdnprofiles.svg new file mode 100644 index 000000000..a0460d1aa --- /dev/null +++ b/icons/cdnprofiles.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/icons/keyvaults.svg b/icons/keyvaults.svg new file mode 100644 index 000000000..81f1f446b --- /dev/null +++ b/icons/keyvaults.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/settings.svg b/icons/settings.svg new file mode 100644 index 000000000..f277de294 --- /dev/null +++ b/icons/settings.svg @@ -0,0 +1,83 @@ + + + + + + + Sheet.1071 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Treeview.ts b/src/Treeview.ts index f73571500..0e911237b 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -38,8 +38,13 @@ const resourceIcons: [string, string][] = [ ["Microsoft.Compute/virtualMachines/extensions", "extensions.svg"], ["Microsoft.Network/networkSecurityGroups", "nsg.svg"], ["Microsoft.Network/networkInterfaces", "nic.svg"], - ["Microsoft.Network/publicIPAddresses", "publicip.svg"] -]; + ["Microsoft.Network/publicIPAddresses", "publicip.svg"], + ["Microsoft.Web/sites", "appservices.svg"], + ["appsettings", "settings.svg"], + ["Microsoft.Insights/components", "applicationinsights.svg"], + ["Microsoft.KeyVault/vaults", "keyvaults.svg"], + ["Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"], + ["Microsoft.Cdn/profiles", "cdnprofiles.svg"]]; export class JsonOutlineProvider implements vscode.TreeDataProvider { private tree: Json.ParseResult | undefined; diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 8cf912e1f..0de0e207e 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -120,6 +120,22 @@ suite("TreeView", async (): Promise => { await testTree(template, expected, ["label"]); } + async function testIcon(resourceType: string, expected: string): Promise { + let template = `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "${resourceType}" + } + ] + }`; + await showNewTextDocument(template); + // tslint:disable-next-line:no-any + let rawTree = getTree(null); + assert.strictEqual(rawTree[2].children![0].icon, expected); + } + function getTree(element?: string): ITestTreeItem[] { let children = provider.getChildren(element); let tree = children.map(child => { @@ -172,6 +188,28 @@ suite("TreeView", async (): Promise => { ///////////////////////// + suite("Icons", () => { + function iconTest(resourceType: string, expectedIcon: string): void { + test(`getIcon: For ${resourceType}`, async () => { + await testIcon(resourceType, expectedIcon); + }); + } + + iconTest("Microsoft.Compute/virtualMachines", "virtualmachines.svg"); + iconTest("Microsoft.Storage/storageAccounts", "storageaccounts.svg"); + iconTest("Microsoft.Network/virtualNetworks", "virtualnetworks.svg"); + iconTest("Microsoft.Compute/virtualMachines/extensions", "extensions.svg"); + iconTest("Microsoft.Network/networkSecurityGroups", "nsg.svg"); + iconTest("Microsoft.Network/networkInterfaces", "nic.svg"); + iconTest("Microsoft.Network/publicIPAddresses", "publicip.svg"); + iconTest("Microsoft.Web/sites", "appservices.svg"); + iconTest("appsettings", "settings.svg"); + iconTest("Microsoft.Insights/components", "applicationinsights.svg"); + iconTest("Microsoft.KeyVault/vaults", "keyvaults.svg"); + iconTest("Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"); + iconTest("Microsoft.Cdn/profiles", "cdnprofiles.svg"); + }); + test("getLabel: displayName tag overrides name", async () => { await testLabels( From b1e2a6da14c69b7acab3a4e95e76bfa36a847af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Fri, 13 Dec 2019 15:22:26 +0100 Subject: [PATCH 02/61] Fixed icon for resource type config --- src/Treeview.ts | 2 +- test/Treeview.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Treeview.ts b/src/Treeview.ts index 0e911237b..914df4620 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -40,7 +40,7 @@ const resourceIcons: [string, string][] = [ ["Microsoft.Network/networkInterfaces", "nic.svg"], ["Microsoft.Network/publicIPAddresses", "publicip.svg"], ["Microsoft.Web/sites", "appservices.svg"], - ["appsettings", "settings.svg"], + ["config", "settings.svg"], ["Microsoft.Insights/components", "applicationinsights.svg"], ["Microsoft.KeyVault/vaults", "keyvaults.svg"], ["Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"], diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 0de0e207e..041ddf9e9 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -203,7 +203,7 @@ suite("TreeView", async (): Promise => { iconTest("Microsoft.Network/networkInterfaces", "nic.svg"); iconTest("Microsoft.Network/publicIPAddresses", "publicip.svg"); iconTest("Microsoft.Web/sites", "appservices.svg"); - iconTest("appsettings", "settings.svg"); + iconTest("config", "settings.svg"); iconTest("Microsoft.Insights/components", "applicationinsights.svg"); iconTest("Microsoft.KeyVault/vaults", "keyvaults.svg"); iconTest("Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"); From 5057095a6687469b76e98caf66a1b8de65ae1184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Sat, 14 Dec 2019 14:41:22 +0100 Subject: [PATCH 03/61] Changed icon for "config" resource type --- icons/appconfiguration.svg | 10 +++++ icons/settings.svg | 83 -------------------------------------- src/Treeview.ts | 2 +- test/Treeview.test.ts | 2 +- 4 files changed, 12 insertions(+), 85 deletions(-) create mode 100644 icons/appconfiguration.svg delete mode 100644 icons/settings.svg diff --git a/icons/appconfiguration.svg b/icons/appconfiguration.svg new file mode 100644 index 000000000..bf1b263a9 --- /dev/null +++ b/icons/appconfiguration.svg @@ -0,0 +1,10 @@ + + + +Azure_AppConfiguration + + + + + + \ No newline at end of file diff --git a/icons/settings.svg b/icons/settings.svg deleted file mode 100644 index f277de294..000000000 --- a/icons/settings.svg +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - Sheet.1071 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Treeview.ts b/src/Treeview.ts index 914df4620..c33b66b42 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -40,7 +40,7 @@ const resourceIcons: [string, string][] = [ ["Microsoft.Network/networkInterfaces", "nic.svg"], ["Microsoft.Network/publicIPAddresses", "publicip.svg"], ["Microsoft.Web/sites", "appservices.svg"], - ["config", "settings.svg"], + ["config", "appconfiguration.svg"], ["Microsoft.Insights/components", "applicationinsights.svg"], ["Microsoft.KeyVault/vaults", "keyvaults.svg"], ["Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"], diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 041ddf9e9..6fe1e0136 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -203,7 +203,7 @@ suite("TreeView", async (): Promise => { iconTest("Microsoft.Network/networkInterfaces", "nic.svg"); iconTest("Microsoft.Network/publicIPAddresses", "publicip.svg"); iconTest("Microsoft.Web/sites", "appservices.svg"); - iconTest("config", "settings.svg"); + iconTest("config", "appconfiguration.svg"); iconTest("Microsoft.Insights/components", "applicationinsights.svg"); iconTest("Microsoft.KeyVault/vaults", "keyvaults.svg"); iconTest("Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"); From c0cc1365a18ae979f15650572e1b61867abbf656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Sun, 15 Dec 2019 00:12:27 +0100 Subject: [PATCH 04/61] Added icon for functions. Refactored and improved icon tests. --- icons/functions.svg | 2822 +++++++++++++++++++++++++++++++++++++++++ src/Treeview.ts | 2 + src/constants.ts | 1 + test/Treeview.test.ts | 133 +- 4 files changed, 2926 insertions(+), 32 deletions(-) create mode 100644 icons/functions.svg diff --git a/icons/functions.svg b/icons/functions.svg new file mode 100644 index 000000000..0c0c90a01 --- /dev/null +++ b/icons/functions.svg @@ -0,0 +1,2822 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +f(n) + diff --git a/src/Treeview.ts b/src/Treeview.ts index c33b66b42..f2a57a7c9 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -21,6 +21,7 @@ const topLevelIcons: [string, string][] = [ ["handler", "label.svg"], [templateKeys.parameters, "parameters.svg"], [templateKeys.variables, "variables.svg"], + [templateKeys.functions, "functions.svg"], ["resources", "resources.svg"], ["outputs", "outputs.svg"], ]; @@ -28,6 +29,7 @@ const topLevelIcons: [string, string][] = [ const topLevelChildIconsByRootNode: [string, string][] = [ [templateKeys.parameters, "parameters.svg"], [templateKeys.variables, "variables.svg"], + [templateKeys.functions, "functions.svg"], ["outputs", "outputs.svg"], ]; diff --git a/src/constants.ts b/src/constants.ts index f0fff353a..1daf0781f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -47,6 +47,7 @@ export namespace templateKeys { export const parameters = 'parameters'; export const resources = 'resources'; export const variables = 'variables'; + export const functions = 'functions'; export const apiProfile = 'apiProfile'; // Copy blocks diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 6fe1e0136..e48529afb 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -120,20 +120,8 @@ suite("TreeView", async (): Promise => { await testTree(template, expected, ["label"]); } - async function testIcon(resourceType: string, expected: string): Promise { - let template = `{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [ - { - "type": "${resourceType}" - } - ] - }`; - await showNewTextDocument(template); - // tslint:disable-next-line:no-any - let rawTree = getTree(null); - assert.strictEqual(rawTree[2].children![0].icon, expected); + async function testIcons(template: string, expected: Partial[]): Promise { + await testTree(template, expected, ["icon"]); } function getTree(element?: string): ITestTreeItem[] { @@ -188,28 +176,109 @@ suite("TreeView", async (): Promise => { ///////////////////////// - suite("Icons", () => { - function iconTest(resourceType: string, expectedIcon: string): void { - test(`getIcon: For ${resourceType}`, async () => { - await testIcon(resourceType, expectedIcon); - }); + test("getIcon: display correct icon", async () => { + interface IconObject { + icon: string; + children: ChildIconObject[]; + } + interface ChildIconObject { + icon: string | undefined; } - iconTest("Microsoft.Compute/virtualMachines", "virtualmachines.svg"); - iconTest("Microsoft.Storage/storageAccounts", "storageaccounts.svg"); - iconTest("Microsoft.Network/virtualNetworks", "virtualnetworks.svg"); - iconTest("Microsoft.Compute/virtualMachines/extensions", "extensions.svg"); - iconTest("Microsoft.Network/networkSecurityGroups", "nsg.svg"); - iconTest("Microsoft.Network/networkInterfaces", "nic.svg"); - iconTest("Microsoft.Network/publicIPAddresses", "publicip.svg"); - iconTest("Microsoft.Web/sites", "appservices.svg"); - iconTest("config", "appconfiguration.svg"); - iconTest("Microsoft.Insights/components", "applicationinsights.svg"); - iconTest("Microsoft.KeyVault/vaults", "keyvaults.svg"); - iconTest("Microsoft.KeyVault/vaults/secrets", "keyvaults.svg"); - iconTest("Microsoft.Cdn/profiles", "cdnprofiles.svg"); + function getIconObject(icon: string, childIcon: string | undefined): IconObject { + return { icon: icon, children: [{ icon: childIcon }] }; + } + + await testIcons( + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "parameter1": { + } + }, + "variables": { + "variable1": "value" + }, + "functions": [ + { + "namespace": "namespace-name" + } + ], + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + }, + { + "type": "Microsoft.Storage/storageAccounts", + }, + { + "type": "Microsoft.Network/virtualNetworks", + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + }, + { + "type": "Microsoft.Network/networkInterfaces", + }, + { + "type": "Microsoft.Network/publicIPAddresses", + }, + { + "type": "Microsoft.Web/sites", + }, + { + "type": "config", + }, + { + "type": "Microsoft.Insights/components", + }, + { + "type": "Microsoft.KeyVault/vaults", + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + }, + { + "type": "Microsoft.Cdn/profiles", + } + ], + "outputs": { + "output1": { + } + } + }`, + [{ icon: "label.svg" }, + { icon: "label.svg" }, + getIconObject("parameters.svg", "parameters.svg"), + getIconObject("variables.svg", "variables.svg"), + { icon: "functions.svg", children: [{ icon: "functions.svg", children: [{ icon: undefined }] }] }, + { + icon: "resources.svg", children: [ + getIconObject("virtualmachines.svg", undefined), + getIconObject("storageaccounts.svg", undefined), + getIconObject("virtualnetworks.svg", undefined), + getIconObject("extensions.svg", undefined), + getIconObject("nsg.svg", undefined), + getIconObject("nic.svg", undefined), + getIconObject("publicip.svg", undefined), + getIconObject("appservices.svg", undefined), + getIconObject("appconfiguration.svg", undefined), + getIconObject("applicationinsights.svg", undefined), + getIconObject("keyvaults.svg", undefined), + getIconObject("keyvaults.svg", undefined), + getIconObject("cdnprofiles.svg", undefined)] + }, + getIconObject("outputs.svg", "outputs.svg")] + , + ); }); + ///////////////////////// + test("getLabel: displayName tag overrides name", async () => { await testLabels( From bce46a8041ff8d6de83b748fdca2035dd0dc9d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Tue, 17 Dec 2019 16:36:44 +0100 Subject: [PATCH 05/61] Added test for current icons for functions in Treeview --- test/Treeview.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index e48529afb..7e5b0c838 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -202,7 +202,19 @@ suite("TreeView", async (): Promise => { }, "functions": [ { - "namespace": "namespace-name" + "namespace": "udf", + "members": { + "storageUri": { + "parameters": [ + { + "name": "storageAccountName", + "type": "string" + }], + "output": { + "value": "[parameters('storageAccountName')]" + } + } + } } ], "resources": [ @@ -255,7 +267,43 @@ suite("TreeView", async (): Promise => { { icon: "label.svg" }, getIconObject("parameters.svg", "parameters.svg"), getIconObject("variables.svg", "variables.svg"), - { icon: "functions.svg", children: [{ icon: "functions.svg", children: [{ icon: undefined }] }] }, + { + icon: "functions.svg", + children: [{ + icon: "functions.svg", + children: [ + { + icon: undefined + }, + { + icon: undefined, + children: [{ + icon: undefined, + children: [{ + icon: undefined, + children: [{ + icon: undefined, + children: [{ + icon: undefined + }, + { + icon: undefined + }] + }], + }, + { + icon: undefined, + children: [ + { + icon: undefined + } + ] + + }], + }] + }] + }] + }, { icon: "resources.svg", children: [ getIconObject("virtualmachines.svg", undefined), From 358a140e7c5e3f6b11e37c9e69bfb49f8e9f8b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Tue, 17 Dec 2019 18:51:28 +0100 Subject: [PATCH 06/61] TreeView now shows icons for functions on 6 levels --- src/Treeview.ts | 35 ++++++++++++++++++++++++++++++++--- test/Treeview.test.ts | 10 +++++----- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Treeview.ts b/src/Treeview.ts index f2a57a7c9..31af63659 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -29,10 +29,14 @@ const topLevelIcons: [string, string][] = [ const topLevelChildIconsByRootNode: [string, string][] = [ [templateKeys.parameters, "parameters.svg"], [templateKeys.variables, "variables.svg"], - [templateKeys.functions, "functions.svg"], ["outputs", "outputs.svg"], ]; +const functionIcons: [string, string][] = [ + [templateKeys.parameters, "parameters.svg"], + ["output", "outputs.svg"], +]; + const resourceIcons: [string, string][] = [ ["Microsoft.Compute/virtualMachines", "virtualmachines.svg"], ["Microsoft.Storage/storageAccounts", "storageaccounts.svg"], @@ -340,10 +344,11 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { // If resourceType element is found on resource objects set to specific resourceType Icon or else a default resource icon // tslint:disable-next-line: strict-boolean-expressions - if (elementInfo.current.level && elementInfo.current.level > 1 && elementInfo.current.key.kind === Json.ValueKind.ObjectValue) { + if (elementInfo.current.level && elementInfo.current.level > 1) { const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start); - if (rootNode && rootNode.toString().toUpperCase() === "resources".toUpperCase() && keyOrResourceNode instanceof Json.ObjectValue) { + if (elementInfo.current.key.kind === Json.ValueKind.ObjectValue && + rootNode && rootNode.toString().toUpperCase() === "resources".toUpperCase() && keyOrResourceNode instanceof Json.ObjectValue) { // tslint:disable-next-line:one-variable-per-declaration for (var i = 0, il = keyOrResourceNode.properties.length; i < il; i++) { const name = keyOrResourceNode.properties[i].nameValue; @@ -356,6 +361,10 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { } } } + + if (rootNode && rootNode.toString().toUpperCase() === "functions".toUpperCase()) { + icon = this.getFunctionsIcon(elementInfo, keyOrResourceNode); + } } if (icon) { @@ -365,6 +374,26 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { return undefined; } + private getFunctionsIcon(elementInfo: IElementInfo, node: Json.Value | null | undefined): string | undefined { + const level: number | undefined = elementInfo.current.level; + if (!elementInfo.current.collapsible || !node || level === undefined) { + return undefined; + } + if (level < 5) { + return this.getIcon(topLevelIcons, templateKeys.functions, ""); + } + if (level === 5) { + return this.getIcon(functionIcons, node.toFriendlyString(), ""); + } + if (elementInfo.current.level === 6 && elementInfo.parent.key.start !== undefined) { + const parentNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.parent.key.start); + if (parentNode) { + return this.getIcon(functionIcons, parentNode.toFriendlyString(), ""); + } + } + return undefined; + } + private updateTreeState(): void { const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; const document: vscode.TextDocument | undefined = !!activeEditor ? activeEditor.document : undefined; diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 7e5b0c838..547913585 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -276,13 +276,13 @@ suite("TreeView", async (): Promise => { icon: undefined }, { - icon: undefined, + icon: "functions.svg", children: [{ - icon: undefined, + icon: "functions.svg", children: [{ - icon: undefined, + icon: "parameters.svg", children: [{ - icon: undefined, + icon: "parameters.svg", children: [{ icon: undefined }, @@ -292,7 +292,7 @@ suite("TreeView", async (): Promise => { }], }, { - icon: undefined, + icon: "outputs.svg", children: [ { icon: undefined From 9edc5809fe4fbad97da71550b2c678102eec9240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Tue, 17 Dec 2019 19:33:56 +0100 Subject: [PATCH 07/61] Treeview uses namespace as label if name and displayName is not present --- src/Treeview.ts | 34 +++++++++++++++++++++------------- test/Treeview.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/Treeview.ts b/src/Treeview.ts index 31af63659..688d5431e 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -175,7 +175,6 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { // Key is an object (e.g. a resource object) if (keyNode instanceof Json.ObjectValue) { - let foundName = false; // Object contains no elements if (keyNode.properties.length === 0) { return "{}"; @@ -194,22 +193,18 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { } } - // Look for name element - // tslint:disable-next-line:one-variable-per-declaration - for (var i = 0, l = keyNode.properties.length; i < l; i++) { - let props = keyNode.properties[i]; - // If name element is found - if (props.nameValue instanceof Json.StringValue && props.nameValue.toString().toUpperCase() === "name".toUpperCase()) { - let name = toFriendlyString(props.value); + let label = this.getLabelFromProperties("name", keyNode); + if (label !== undefined) { + return label; + } - return shortenTreeLabel(name); - } + label = this.getLabelFromProperties("namespace", keyNode); + if (label !== undefined) { + return label; } // Object contains elements, but not a name element - if (!foundName) { - return "{...}"; - } + return "{...}"; } } else if (elementInfo.current.value.kind === Json.ValueKind.ArrayValue || elementInfo.current.value.kind === Json.ValueKind.ObjectValue) { @@ -225,6 +220,19 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { return ""; } + private getLabelFromProperties(propertyName: string, keyNode: Json.ObjectValue): string | undefined { + // tslint:disable-next-line:one-variable-per-declaration + for (var i = 0, l = keyNode.properties.length; i < l; i++) { + let props = keyNode.properties[i]; + // If element is found + if (props.nameValue instanceof Json.StringValue && props.nameValue.toString().toUpperCase() === propertyName.toUpperCase()) { + let name = toFriendlyString(props.value); + return shortenTreeLabel(name); + } + } + return undefined; + } + /** * Returns an IElementInfo that describes either an array element or an object element (a property) */ diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index 547913585..ad41f36ac 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -392,6 +392,44 @@ suite("TreeView", async (): Promise => { ///////////////////////// + test("getLabel: namespace used if name and displayName is not present", async () => { + + await testLabels( + `{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "Functions": [ + { + "namespace": "udf", + } + ] + }`, + [ + { + label: "$schema: http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + }, + { + label: "contentVersion: 1.0.0.0", + }, + { + label: "Functions", + children: [ + { + label: "udf", + children: [ + { + label: "namespace: udf", + } + ] + } + ] + } + ] + ); + }); + + ///////////////////////// + test("getChildren: Full tree: all default param types", async () => { await testTree( templateAllParamDefaultTypes, From 66bda6fc64595ce917c0b0279abb2a911f862b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Tue, 17 Dec 2019 20:53:08 +0100 Subject: [PATCH 08/61] Fixed bug when getting icons for a function without parameters --- src/Treeview.ts | 18 +++++++++--------- test/Treeview.test.ts | 27 +++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Treeview.ts b/src/Treeview.ts index 688d5431e..00490c0ce 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -384,20 +384,20 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { private getFunctionsIcon(elementInfo: IElementInfo, node: Json.Value | null | undefined): string | undefined { const level: number | undefined = elementInfo.current.level; - if (!elementInfo.current.collapsible || !node || level === undefined) { + if (!node || level === undefined) { return undefined; } - if (level < 5) { - return this.getIcon(topLevelIcons, templateKeys.functions, ""); - } if (level === 5) { return this.getIcon(functionIcons, node.toFriendlyString(), ""); } - if (elementInfo.current.level === 6 && elementInfo.parent.key.start !== undefined) { - const parentNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.parent.key.start); - if (parentNode) { - return this.getIcon(functionIcons, parentNode.toFriendlyString(), ""); - } + if (!elementInfo.current.collapsible) { + return undefined; + } + if (level < 5) { + return this.getIcon(topLevelIcons, templateKeys.functions, ""); + } + if (elementInfo.current.level === 6) { + return this.getIcon(functionIcons, "parameters", ""); } return undefined; } diff --git a/test/Treeview.test.ts b/test/Treeview.test.ts index ad41f36ac..7d2dffda1 100644 --- a/test/Treeview.test.ts +++ b/test/Treeview.test.ts @@ -213,7 +213,15 @@ suite("TreeView", async (): Promise => { "output": { "value": "[parameters('storageAccountName')]" } - } + }, + "getSubscriptionId": { + "parameters": [ + ], + "output": { + "type": "string", + "value": "[subscription().subscriptionId]" + } + } } } ], @@ -298,7 +306,22 @@ suite("TreeView", async (): Promise => { icon: undefined } ] - + }], + }, { + icon: "functions.svg", + children: [{ + icon: "parameters.svg", + }, + { + icon: "outputs.svg", + children: [ + { + icon: undefined + }, + { + icon: undefined + } + ] }], }] }] From 2d25b01c4f967b6c99b58841c7ce51f8c35f7ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Hedstr=C3=B6m?= Date: Tue, 11 Feb 2020 02:53:39 +0100 Subject: [PATCH 09/61] Updating TreeView after setting language to arm-template. --- src/Treeview.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Treeview.ts b/src/Treeview.ts index 00490c0ce..f5aba9cf6 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -63,6 +63,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { constructor(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => this.updateTreeState())); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(() => this.updateTreeState())); + context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(() => this.updateTreeState())); setTimeout( () => { From e26de41ed4aa1f50d3458194442169697a7d1275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Tue, 31 Mar 2020 23:54:10 +0200 Subject: [PATCH 10/61] Start of insertItem --- package.json | 19 ++++++++++++++- src/AzureRMTools.ts | 22 ++++++++++++++++++ src/insertItem.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/insertItem.ts diff --git a/package.json b/package.json index 7dc8e89b9..d84f1c310 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "onCommand:azurerm-vscode-tools.sortVariables", "onCommand:azurerm-vscode-tools.selectParameterFile", "onCommand:azurerm-vscode-tools.openParameterFile", - "onCommand:azurerm-vscode-tools.resetGlobalState" + "onCommand:azurerm-vscode-tools.resetGlobalState", + "onCommand:azurerm-vscode-tools.insertItem" ], "contributes": { "grammars": [ @@ -206,6 +207,12 @@ "category": "Azure Resource Manager Tools", "title": "Reset Global State", "command": "azurerm-vscode-tools.resetGlobalState" + }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert item", + "command": "azurerm-vscode-tools.insertItem", + "enablement": "editorLangId==arm-template" } ], "menus": { @@ -246,6 +253,11 @@ "command": "azurerm-vscode-tools.sortTemplate", "when": "editorLangId==arm-template", "group": "zzz_arm-template@3" + }, + { + "command": "azurerm-vscode-tools.insertItem", + "when": "editorLangId==arm-template", + "group": "zzz_arm-template@4" } ], "view/item/context": [ @@ -278,6 +290,11 @@ "command": "azurerm-vscode-tools.sortVariables", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertItem", + "when": "azurerm-vscode-tools.template-outline.active == true", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 4070deb74..e249b4f6f 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -19,6 +19,7 @@ import { Histogram } from "./Histogram"; import * as Hover from './Hover'; import { DefinitionKind } from "./INamedDefinition"; import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; +import { getInsertItemQuickPickItems, insertItem } from "./insertItem"; import * as Json from "./JSON"; import * as language from "./Language"; import { reloadSchemas } from "./languageclient/reloadSchemas"; @@ -127,6 +128,18 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.sortTopLevel", async () => { await this.sortTemplate(SortType.TopLevel); }); + registerCommand("azurerm-vscode-tools.insertItem", async (_context: 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 sortType = await ext.ui.showQuickPick(getInsertItemQuickPickItems(), { placeHolder: 'What do you want to insert?' }); + await this.insertItem(sortType.value, uri, editor); + } + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); @@ -158,6 +171,15 @@ export class AzureRMTools { } } + private async insertItem(sortType: SortType, 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.getDeploymentTemplate(editor.document); + await insertItem(deploymentTemplate, sortType, editor); + } + } + public dispose(): void { callWithTelemetryAndErrorHandlingSync('dispose', (actionContext: IActionContext): void => { actionContext.telemetry.properties.isActivationEvent = 'true'; diff --git a/src/insertItem.ts b/src/insertItem.ts new file mode 100644 index 000000000..41843649a --- /dev/null +++ b/src/insertItem.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from "vscode"; +import { Json, templateKeys } from "../extension.bundle"; +import { DeploymentTemplate } from "./DeploymentTemplate"; +import { ext } from './extensionVariables'; +import { SortQuickPickItem, SortType } from "./sortTemplate"; + +export function getInsertItemQuickPickItems(): SortQuickPickItem[] { + let items: SortQuickPickItem[] = []; + items.push(new SortQuickPickItem("Function", SortType.Functions, "Insert a function")); + items.push(new SortQuickPickItem("Output", SortType.Outputs, "Inserts an output")); + items.push(new SortQuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); + items.push(new SortQuickPickItem("Resource", SortType.Resources, "Insert a resource")); + items.push(new SortQuickPickItem("Variable", SortType.Variables, "Insert a variable")); + return items; +} + +export async function insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { + if (!template) { + return; + } + ext.outputChannel.appendLine("Insert item"); + switch (sortType) { + case SortType.Functions: + break; + case SortType.Outputs: + break; + case SortType.Parameters: + let rootValue = template.topLevelValue; + if (!rootValue) { + return; + } + let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); + let index = parameters?.span.afterEndIndex; + if (index !== undefined) { + await textEditor.edit(builder => { + let i: number = index!; + let pos = textEditor.document.positionAt(i); + builder.insert(pos, "Hello world!"); + }); + } + break; + case SortType.Resources: + break; + case SortType.Variables: + break; + default: + vscode.window.showWarningMessage("Unknown insert item type!"); + return; + } + vscode.window.showInformationMessage("Done inserting item!"); +} From 03f642d018bf8547140b8081789e66a7f9981254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 11:04:14 +0200 Subject: [PATCH 11/61] First working implementation of "Insert parameter" --- package.json | 16 +++++++++++++--- src/AzureRMTools.ts | 7 +++++-- src/insertItem.ts | 46 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index d84f1c310..2610bba38 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "onCommand:azurerm-vscode-tools.selectParameterFile", "onCommand:azurerm-vscode-tools.openParameterFile", "onCommand:azurerm-vscode-tools.resetGlobalState", - "onCommand:azurerm-vscode-tools.insertItem" + "onCommand:azurerm-vscode-tools.insertItem", + "onCommand:azurerm-vscode-tools.insertParameter" ], "contributes": { "grammars": [ @@ -211,8 +212,12 @@ { "category": "Azure Resource Manager Tools", "title": "Insert item", - "command": "azurerm-vscode-tools.insertItem", - "enablement": "editorLangId==arm-template" + "command": "azurerm-vscode-tools.insertItem" + }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert parameter", + "command": "azurerm-vscode-tools.insertParameter" } ], "menus": { @@ -295,6 +300,11 @@ "command": "azurerm-vscode-tools.insertItem", "when": "azurerm-vscode-tools.template-outline.active == true", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertParameter", + "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index e249b4f6f..fd208e722 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -19,7 +19,7 @@ import { Histogram } from "./Histogram"; import * as Hover from './Hover'; import { DefinitionKind } from "./INamedDefinition"; import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; -import { getInsertItemQuickPickItems, insertItem } from "./insertItem"; +import { getInsertItemType, insertItem } from "./insertItem"; import * as Json from "./JSON"; import * as language from "./Language"; import { reloadSchemas } from "./languageclient/reloadSchemas"; @@ -136,10 +136,13 @@ export class AzureRMTools { uri = vscode.window.activeTextEditor?.document.uri; } if (uri && editor) { - const sortType = await ext.ui.showQuickPick(getInsertItemQuickPickItems(), { placeHolder: 'What do you want to insert?' }); + const sortType = await ext.ui.showQuickPick(getInsertItemType(), { placeHolder: 'What do you want to insert?' }); await this.insertItem(sortType.value, uri, editor); } }); + registerCommand("azurerm-vscode-tools.insertParameter", async () => { + await this.insertItem(SortType.Parameters); + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); diff --git a/src/insertItem.ts b/src/insertItem.ts index 41843649a..be051f574 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -7,15 +7,39 @@ import * as vscode from "vscode"; import { Json, templateKeys } from "../extension.bundle"; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; -import { SortQuickPickItem, SortType } from "./sortTemplate"; +import { SortType } from "./sortTemplate"; -export function getInsertItemQuickPickItems(): SortQuickPickItem[] { - let items: SortQuickPickItem[] = []; - items.push(new SortQuickPickItem("Function", SortType.Functions, "Insert a function")); - items.push(new SortQuickPickItem("Output", SortType.Outputs, "Inserts an output")); - items.push(new SortQuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); - items.push(new SortQuickPickItem("Resource", SortType.Resources, "Insert a resource")); - items.push(new SortQuickPickItem("Variable", SortType.Variables, "Insert a variable")); +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 getParameterTypeType(): 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; +} + +export function getInsertItemType(): QuickPickItem[] { + let items: QuickPickItem[] = []; + // items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); + // items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); + items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); + // items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); + // items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); return items; } @@ -35,12 +59,14 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT return; } let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); - let index = parameters?.span.afterEndIndex; + let name = await ext.ui.showInputBox({ prompt: "Name of parameter" }); + const parameterType = await ext.ui.showQuickPick(getParameterTypeType(), { placeHolder: 'Type of parameter?' }); + let index = parameters?.span.endIndex; if (index !== undefined) { await textEditor.edit(builder => { let i: number = index!; let pos = textEditor.document.positionAt(i); - builder.insert(pos, "Hello world!"); + builder.insert(pos, "\t,\"" + name + "\": {\r\n\t\t\t\"type\": \"" + parameterType.value + "\"\r\n\t\t}\r\n\t"); }); } break; From 07da1432ed779b6d09fcefe530660f27775e1087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 11:34:25 +0200 Subject: [PATCH 12/61] Todo for insert parameter --- src/insertItem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/insertItem.ts b/src/insertItem.ts index be051f574..182c82449 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -61,6 +61,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); let name = await ext.ui.showInputBox({ prompt: "Name of parameter" }); const parameterType = await ext.ui.showQuickPick(getParameterTypeType(), { placeHolder: 'Type of parameter?' }); + //TODO: Should we ask about description and default value? let index = parameters?.span.endIndex; if (index !== undefined) { await textEditor.edit(builder => { From af858274617d3e541f18ad9eabe16dd4eace499f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 11:40:40 +0200 Subject: [PATCH 13/61] Changed to template literal --- src/insertItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 182c82449..2e55df06a 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -67,7 +67,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT await textEditor.edit(builder => { let i: number = index!; let pos = textEditor.document.positionAt(i); - builder.insert(pos, "\t,\"" + name + "\": {\r\n\t\t\t\"type\": \"" + parameterType.value + "\"\r\n\t\t}\r\n\t"); + builder.insert(pos, `\t,"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"\r\n\t\t\}\r\n\t`); }); } break; From d875c95ac71453ec730d2fcf7ca73b13972ef1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 20:03:01 +0200 Subject: [PATCH 14/61] Added possibility to specify default value and description of parameter --- src/insertItem.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 2e55df06a..e3e230923 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -21,7 +21,7 @@ export class QuickPickItem implements vscode.QuickPickItem { } } -export function getParameterTypeType(): QuickPickItem[] { +export function getParameterType(): QuickPickItem[] { let items: QuickPickItem[] = []; items.push(new QuickPickItem("String", "string", "A string")); items.push(new QuickPickItem("Secure string", "securestring", "A secure string")); @@ -59,15 +59,19 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT return; } let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); - let name = await ext.ui.showInputBox({ prompt: "Name of parameter" }); - const parameterType = await ext.ui.showQuickPick(getParameterTypeType(), { placeHolder: 'Type of parameter?' }); - //TODO: Should we ask about description and default value? + let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); + const parameterType = await ext.ui.showQuickPick(getParameterType(), { placeHolder: 'Type of parameter?' }); + let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); + let defaultValueText = defaultValue === '' ? '' : `,\r\n\t\t\t"defaultValue": "${defaultValue}"`; + let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); + let descriptionValueText = descriptionValue === '' ? '' : `,\r\n\t\t\t"metadata": {\r\n\t\t\t\t"description": "${descriptionValue}"\r\n\t\t\t}`; let index = parameters?.span.endIndex; + if (index !== undefined) { await textEditor.edit(builder => { let i: number = index!; let pos = textEditor.document.positionAt(i); - builder.insert(pos, `\t,"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"\r\n\t\t\}\r\n\t`); + builder.insert(pos, `\t,"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`); }); } break; From c8214bdc833b54ec747f712fba7e9994d21a9069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 20:13:26 +0200 Subject: [PATCH 15/61] Handle case when there is no parameters before --- src/insertItem.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index e3e230923..734aea95e 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -59,6 +59,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT return; } let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); + let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); const parameterType = await ext.ui.showQuickPick(getParameterType(), { placeHolder: 'Type of parameter?' }); let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); @@ -71,7 +72,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT await textEditor.edit(builder => { let i: number = index!; let pos = textEditor.document.positionAt(i); - builder.insert(pos, `\t,"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`); + builder.insert(pos, `${startText}"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`); }); } break; From f9bd80634f6471adb2a5e588a13acc4b5ec246fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 1 Apr 2020 20:44:07 +0200 Subject: [PATCH 16/61] Initial support for 'Insert variable' --- package.json | 21 +++++++++++++- src/AzureRMTools.ts | 3 ++ src/insertItem.ts | 67 ++++++++++++++++++++++++++++++--------------- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 2610bba38..1e67dd732 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "onCommand:azurerm-vscode-tools.openParameterFile", "onCommand:azurerm-vscode-tools.resetGlobalState", "onCommand:azurerm-vscode-tools.insertItem", - "onCommand:azurerm-vscode-tools.insertParameter" + "onCommand:azurerm-vscode-tools.insertParameter", + "onCommand:azurerm-vscode-tools.insertVariable" ], "contributes": { "grammars": [ @@ -214,6 +215,11 @@ "title": "Insert item", "command": "azurerm-vscode-tools.insertItem" }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert variable", + "command": "azurerm-vscode-tools.insertVariable" + }, { "category": "Azure Resource Manager Tools", "title": "Insert parameter", @@ -241,6 +247,14 @@ { "command": "azurerm-vscode-tools.sortVariables", "when": "never" + }, + { + "command": "azurerm-vscode-tools.insertParameter", + "when": "never" + }, + { + "command": "azurerm-vscode-tools.insertVariable", + "when": "never" } ], "editor/context": [ @@ -305,6 +319,11 @@ "command": "azurerm-vscode-tools.insertParameter", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertVariable", + "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index fd208e722..f64f74e22 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -143,6 +143,9 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.insertParameter", async () => { await this.insertItem(SortType.Parameters); }); + registerCommand("azurerm-vscode-tools.insertVariable", async () => { + await this.insertItem(SortType.Variables); + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); diff --git a/src/insertItem.ts b/src/insertItem.ts index 734aea95e..16653aa7c 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -39,7 +39,7 @@ export function getInsertItemType(): QuickPickItem[] { // items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); // items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); - // items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); + items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); return items; } @@ -54,31 +54,12 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT case SortType.Outputs: break; case SortType.Parameters: - let rootValue = template.topLevelValue; - if (!rootValue) { - return; - } - let parameters = Json.asObjectValue(rootValue.getPropertyValue(templateKeys.parameters)); - let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; - let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); - const parameterType = await ext.ui.showQuickPick(getParameterType(), { placeHolder: 'Type of parameter?' }); - let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); - let defaultValueText = defaultValue === '' ? '' : `,\r\n\t\t\t"defaultValue": "${defaultValue}"`; - let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); - let descriptionValueText = descriptionValue === '' ? '' : `,\r\n\t\t\t"metadata": {\r\n\t\t\t\t"description": "${descriptionValue}"\r\n\t\t\t}`; - let index = parameters?.span.endIndex; - - if (index !== undefined) { - await textEditor.edit(builder => { - let i: number = index!; - let pos = textEditor.document.positionAt(i); - builder.insert(pos, `${startText}"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`); - }); - } + await insertParameter(template, textEditor); break; case SortType.Resources: break; case SortType.Variables: + await insertVariable(template, textEditor); break; default: vscode.window.showWarningMessage("Unknown insert item type!"); @@ -86,3 +67,45 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT } vscode.window.showInformationMessage("Done inserting item!"); } + +function getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { + let rootValue = template.topLevelValue; + if (!rootValue) { + return undefined; + } + let parameters = Json.asObjectValue(rootValue.getPropertyValue(templatePart)); + return parameters; +} + +async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let parameters = getTemplatePart(template, templateKeys.parameters); + let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); + const parameterType = await ext.ui.showQuickPick(getParameterType(), { placeHolder: 'Type of parameter?' }); + let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); + let defaultValueText = defaultValue === '' ? '' : `,\r\n\t\t\t"defaultValue": "${defaultValue}"`; + let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); + let descriptionValueText = descriptionValue === '' ? '' : `,\r\n\t\t\t"metadata": {\r\n\t\t\t\t"description": "${descriptionValue}"\r\n\t\t\t}`; + let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`; + let index = parameters?.span.endIndex; + await insertText(textEditor, index, text); +} + +async function insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let variables = getTemplatePart(template, templateKeys.variables); + let startText = variables?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + let name = await ext.ui.showInputBox({ prompt: "Name of variable?" }); + let text = `${startText}"${name}": ""\r\n\t`; + let index = variables?.span.endIndex; + await insertText(textEditor, index, text); +} + +async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { + if (index !== undefined) { + await textEditor.edit(builder => { + let i: number = index!; + let pos = textEditor.document.positionAt(i); + builder.insert(pos, text); + }); + } +} From 45614d54fb8ff3d6487ba849c1347cc4b5fece6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Thu, 2 Apr 2020 18:58:36 +0200 Subject: [PATCH 17/61] InsertVariable sets the cursor position in the variable value --- src/insertItem.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/insertItem.ts b/src/insertItem.ts index 16653aa7c..480894d12 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -98,6 +98,10 @@ async function insertVariable(template: DeploymentTemplate, textEditor: vscode.T let text = `${startText}"${name}": ""\r\n\t`; let index = variables?.span.endIndex; await insertText(textEditor, index, text); + let cursorPos = text.indexOf('""'); + let pos = textEditor.document.positionAt(index! + cursorPos + 1); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; } async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { From 873def777b6eb85e3b9ec9420670176c684c9ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Thu, 2 Apr 2020 23:33:28 +0200 Subject: [PATCH 18/61] insertVariable now scrolls to cursor if needed --- src/insertItem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/insertItem.ts b/src/insertItem.ts index 480894d12..ae9adca94 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -102,6 +102,7 @@ async function insertVariable(template: DeploymentTemplate, textEditor: vscode.T let pos = textEditor.document.positionAt(index! + cursorPos + 1); let newSelection = new vscode.Selection(pos, pos); textEditor.selection = newSelection; + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { From 7cfb95a4fa9ebd7e88c4c87800ea75e317d3a070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 3 Apr 2020 18:01:29 +0200 Subject: [PATCH 19/61] Implemented Insert Output --- package.json | 23 +++++++++++++++++++---- src/AzureRMTools.ts | 3 +++ src/insertItem.ts | 22 +++++++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d00cb8b65..b2371c313 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "onCommand:azurerm-vscode-tools.resetGlobalState", "onCommand:azurerm-vscode-tools.insertItem", "onCommand:azurerm-vscode-tools.insertParameter", - "onCommand:azurerm-vscode-tools.insertVariable" + "onCommand:azurerm-vscode-tools.insertVariable", + "onCommand:azurerm-vscode-tools.insertOutput" ], "contributes": { "grammars": [ @@ -212,18 +213,23 @@ }, { "category": "Azure Resource Manager Tools", - "title": "Insert item", + "title": "Insert Item...", "command": "azurerm-vscode-tools.insertItem" }, { "category": "Azure Resource Manager Tools", - "title": "Insert variable", + "title": "Insert Variable", "command": "azurerm-vscode-tools.insertVariable" }, { "category": "Azure Resource Manager Tools", - "title": "Insert parameter", + "title": "Insert Parameter", "command": "azurerm-vscode-tools.insertParameter" + }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert Output", + "command": "azurerm-vscode-tools.insertOutput" } ], "menus": { @@ -255,6 +261,10 @@ { "command": "azurerm-vscode-tools.insertVariable", "when": "never" + }, + { + "command": "azurerm-vscode-tools.insertOutput", + "when": "never" } ], "editor/context": [ @@ -324,6 +334,11 @@ "command": "azurerm-vscode-tools.insertVariable", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertOutput", + "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index f64f74e22..6222afe6b 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -146,6 +146,9 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.insertVariable", async () => { await this.insertItem(SortType.Variables); }); + registerCommand("azurerm-vscode-tools.insertOutput", async () => { + await this.insertItem(SortType.Outputs); + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); diff --git a/src/insertItem.ts b/src/insertItem.ts index ae9adca94..baddb1e49 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -21,7 +21,7 @@ export class QuickPickItem implements vscode.QuickPickItem { } } -export function getParameterType(): QuickPickItem[] { +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")); @@ -36,7 +36,7 @@ export function getParameterType(): QuickPickItem[] { export function getInsertItemType(): QuickPickItem[] { let items: QuickPickItem[] = []; // items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); - // items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); + items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); // items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); @@ -52,6 +52,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT case SortType.Functions: break; case SortType.Outputs: + await insertOutput(template, textEditor); break; case SortType.Parameters: await insertParameter(template, textEditor); @@ -81,7 +82,7 @@ async function insertParameter(template: DeploymentTemplate, textEditor: vscode. let parameters = getTemplatePart(template, templateKeys.parameters); let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); - const parameterType = await ext.ui.showQuickPick(getParameterType(), { placeHolder: 'Type of parameter?' }); + const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); let defaultValueText = defaultValue === '' ? '' : `,\r\n\t\t\t"defaultValue": "${defaultValue}"`; let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); @@ -105,6 +106,21 @@ async function insertVariable(template: DeploymentTemplate, textEditor: vscode.T textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } +async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let outputs = getTemplatePart(template, templateKeys.outputs); + let startText = outputs?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); + const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); + let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${outputType.value}",\r\n\t\t\t"value": ""\r\n\t\t\}\r\n\t`; + let index = outputs?.span.endIndex; + await insertText(textEditor, index, text); + let cursorPos = text.indexOf('""'); + let pos = textEditor.document.positionAt(index! + cursorPos - 1); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); +} + async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { if (index !== undefined) { await textEditor.edit(builder => { From b02a804ba181a6ce161ebe3308f2f40925a5fc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 4 Apr 2020 10:19:25 +0200 Subject: [PATCH 20/61] Start of insert function --- src/insertItem.ts | 57 +++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index baddb1e49..112ffaab5 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -35,7 +35,7 @@ export function getItemType(): QuickPickItem[] { export function getInsertItemType(): QuickPickItem[] { let items: QuickPickItem[] = []; - // items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); + items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); // items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); @@ -50,6 +50,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT ext.outputChannel.appendLine("Insert item"); switch (sortType) { case SortType.Functions: + await insertFunction(template, textEditor); break; case SortType.Outputs: await insertOutput(template, textEditor); @@ -69,17 +70,26 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT vscode.window.showInformationMessage("Done inserting item!"); } -function getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { +function getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { + let part = getTemplatePart(template, templatePart); + return Json.asObjectValue(part); +} + +function getTemplateArrayPart(template: DeploymentTemplate, templatePart: string): Json.ArrayValue | undefined { + let part = getTemplatePart(template, templatePart); + return Json.asArrayValue(part); +} + +function getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.Value | undefined { let rootValue = template.topLevelValue; if (!rootValue) { return undefined; } - let parameters = Json.asObjectValue(rootValue.getPropertyValue(templatePart)); - return parameters; + return rootValue.getPropertyValue(templatePart); } async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let parameters = getTemplatePart(template, templateKeys.parameters); + let parameters = getTemplateObjectPart(template, templateKeys.parameters); let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); @@ -93,32 +103,32 @@ async function insertParameter(template: DeploymentTemplate, textEditor: vscode. } async function insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let variables = getTemplatePart(template, templateKeys.variables); + let variables = getTemplateObjectPart(template, templateKeys.variables); let startText = variables?.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of variable?" }); let text = `${startText}"${name}": ""\r\n\t`; let index = variables?.span.endIndex; - await insertText(textEditor, index, text); - let cursorPos = text.indexOf('""'); - let pos = textEditor.document.positionAt(index! + cursorPos + 1); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; - textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); + await insertTextAndSetCursor(textEditor, index, text); } async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let outputs = getTemplatePart(template, templateKeys.outputs); + let outputs = getTemplateObjectPart(template, templateKeys.outputs); let startText = outputs?.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${outputType.value}",\r\n\t\t\t"value": ""\r\n\t\t\}\r\n\t`; let index = outputs?.span.endIndex; - await insertText(textEditor, index, text); - let cursorPos = text.indexOf('""'); - let pos = textEditor.document.positionAt(index! + cursorPos - 1); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; - textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); + await insertTextAndSetCursor(textEditor, index, text); +} + +async function insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let functions = getTemplateArrayPart(template, templateKeys.functions); + if (functions?.length === 0) { + let namespace = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); + let text = `\r\n\t\t{\r\n\t\t\t"namespace": "${namespace}",\r\n\t\t\t"members": {\r\n\t\t\t}\r\n\t\t}\r\n\t`; + let index = functions?.span.endIndex; + await insertText(textEditor, index, text); + } } async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { @@ -130,3 +140,12 @@ async function insertText(textEditor: vscode.TextEditor, index: number | undefin }); } } + +async function insertTextAndSetCursor(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { + await insertText(textEditor, index, text); + let cursorPos = text.indexOf('""'); + let pos = textEditor.document.positionAt(index! + cursorPos - 1); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); +} From b1a41e88d62d935000e7988d86ed0336302b3a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 4 Apr 2020 13:17:16 +0200 Subject: [PATCH 21/61] Improved insert function --- package.json | 17 +++++++++- src/AzureRMTools.ts | 3 ++ src/insertItem.ts | 75 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b2371c313..5f949108b 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "onCommand:azurerm-vscode-tools.insertItem", "onCommand:azurerm-vscode-tools.insertParameter", "onCommand:azurerm-vscode-tools.insertVariable", - "onCommand:azurerm-vscode-tools.insertOutput" + "onCommand:azurerm-vscode-tools.insertOutput", + "onCommand:azurerm-vscode-tools.insertFunction" ], "contributes": { "grammars": [ @@ -221,6 +222,11 @@ "title": "Insert Variable", "command": "azurerm-vscode-tools.insertVariable" }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert Function", + "command": "azurerm-vscode-tools.insertFunction" + }, { "category": "Azure Resource Manager Tools", "title": "Insert Parameter", @@ -265,6 +271,10 @@ { "command": "azurerm-vscode-tools.insertOutput", "when": "never" + }, + { + "command": "azurerm-vscode-tools.insertFunction", + "when": "never" } ], "editor/context": [ @@ -339,6 +349,11 @@ "command": "azurerm-vscode-tools.insertOutput", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertFunction", + "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == functions", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 6222afe6b..fdd07c845 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -149,6 +149,9 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.insertOutput", async () => { await this.insertItem(SortType.Outputs); }); + registerCommand("azurerm-vscode-tools.insertFunction", async () => { + await this.insertItem(SortType.Functions); + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); diff --git a/src/insertItem.ts b/src/insertItem.ts index 112ffaab5..f489f396b 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -123,12 +123,62 @@ async function insertOutput(template: DeploymentTemplate, textEditor: vscode.Tex async function insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let functions = getTemplateArrayPart(template, templateKeys.functions); + let index: number = 0; + let text: string; + let namespaceName: string = ""; if (functions?.length === 0) { - let namespace = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); - let text = `\r\n\t\t{\r\n\t\t\t"namespace": "${namespace}",\r\n\t\t\t"members": {\r\n\t\t\t}\r\n\t\t}\r\n\t`; - let index = functions?.span.endIndex; - await insertText(textEditor, index, text); + namespaceName = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); + index = functions?.span.endIndex; + } else { + let namespace = Json.asObjectValue(functions?.elements[0]); + let members2 = namespace?.getPropertyValue("members"); + index = members2?.span.endIndex! - 4; } + let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); + let functionDef = await getFunction(); + if (namespaceName !== '') { + let members: any = {}; + members[functionName] = functionDef; + let namespace = { + namespace: namespaceName, + members: members + }; + text = JSON.stringify(namespace, null, '\t'); + let indentedText = indent(`\r\n${text}\r\n`, 2); + await insertTextAndSetCursor(textEditor, index, indentedText + '\t'); + } else { + text = JSON.stringify(functionDef, null, '\t'); + let indentedText = indent(`"${functionName}": ${text}\r\n`, 4); + await insertTextAndSetCursor(textEditor, index, `,\r\n${indentedText}\t`); + } +} +async function getFunction(): Promise { + const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of function output?' }); + let parameters = await getFunctionParameters(); + let functionDef = { + parameters: parameters, + output: { + type: outputType.value, + value: "" + } + }; + return functionDef; +} +async function getFunctionParameters(): Promise { + let parameterName: string; + let parameters = []; + do { + parameterName = await ext.ui.showInputBox({ prompt: "Name of parameter? Leave empty for no more parameters" }); + if (parameterName !== '') { + const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: `Type of parameter ${parameterName}?` }); + parameters.push({ + name: parameterName, + type: parameterType.value + }); + } + + } while (parameterName !== ''); + return parameters; } async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { @@ -149,3 +199,20 @@ async function insertTextAndSetCursor(textEditor: vscode.TextEditor, index: numb textEditor.selection = newSelection; textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } + +/** + * Indents the given string + * @param {string} str The string to be indented. + * @param {number} numOfIndents The amount of indentations to place at the + * beginning of each line of the string. + * @param {number=} opt_spacesPerIndent Optional. If specified, this should be + * the number of spaces to be used for each tab that would ordinarily be + * used to indent the text. These amount of spaces will also be used to + * replace any tab characters that already exist within the string. + * @return {string} The new string with each line beginning with the desired + * amount of indentation. + */ +function indent(str: string, numOfIndents: number) { + str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t')); + return str; +} From 34763e3138a1cd9547e512f06cc07ded7f1f4236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 4 Apr 2020 17:03:59 +0200 Subject: [PATCH 22/61] Implemented Insert Resource --- package.json | 17 +++++++- src/AzureRMTools.ts | 3 ++ src/insertItem.ts | 100 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5f949108b..bae70b29b 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "onCommand:azurerm-vscode-tools.insertParameter", "onCommand:azurerm-vscode-tools.insertVariable", "onCommand:azurerm-vscode-tools.insertOutput", - "onCommand:azurerm-vscode-tools.insertFunction" + "onCommand:azurerm-vscode-tools.insertFunction", + "onCommand:azurerm-vscode-tools.insertResource" ], "contributes": { "grammars": [ @@ -227,6 +228,11 @@ "title": "Insert Function", "command": "azurerm-vscode-tools.insertFunction" }, + { + "category": "Azure Resource Manager Tools", + "title": "Insert Resource", + "command": "azurerm-vscode-tools.insertResource" + }, { "category": "Azure Resource Manager Tools", "title": "Insert Parameter", @@ -275,6 +281,10 @@ { "command": "azurerm-vscode-tools.insertFunction", "when": "never" + }, + { + "command": "azurerm-vscode-tools.insertResource", + "when": "never" } ], "editor/context": [ @@ -354,6 +364,11 @@ "command": "azurerm-vscode-tools.insertFunction", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == functions", "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.insertResource", + "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == resources", + "group": "arm-template" } ], "editor/title": [ diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index fdd07c845..58f515e83 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -152,6 +152,9 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.insertFunction", async () => { await this.insertItem(SortType.Functions); }); + registerCommand("azurerm-vscode-tools.insertResource", async () => { + await this.insertItem(SortType.Resources); + }); registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); diff --git a/src/insertItem.ts b/src/insertItem.ts index f489f396b..94444713f 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from "vscode"; +import { commands } from "vscode"; import { Json, templateKeys } from "../extension.bundle"; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; @@ -33,12 +34,86 @@ export function getItemType(): QuickPickItem[] { return items; } +export function getResourceSnippets(): vscode.QuickPickItem[] { + let items: vscode.QuickPickItem[] = []; + items.push(getQuickPickItem("Nested Deployment")); + items.push(getQuickPickItem("App Service Plan (Server Farm)")); + items.push(getQuickPickItem("Application Insights for Web Apps")); + items.push(getQuickPickItem("Application Security Group")); + items.push(getQuickPickItem("Automation Account")); + items.push(getQuickPickItem("Automation Certificate")); + items.push(getQuickPickItem("Automation Credential")); + items.push(getQuickPickItem("Automation Job Schedule")); + items.push(getQuickPickItem("Automation Runbook")); + items.push(getQuickPickItem("Automation Schedule")); + items.push(getQuickPickItem("Automation Variable")); + items.push(getQuickPickItem("Automation Module")); + items.push(getQuickPickItem("Availability Set")); + items.push(getQuickPickItem("Azure Firewall")); + items.push(getQuickPickItem("Container Group")); + items.push(getQuickPickItem("Container Registry")); + items.push(getQuickPickItem("Cosmos DB Database Account")); + items.push(getQuickPickItem("Cosmos DB SQL Database")); + items.push(getQuickPickItem("Cosmos DB Mongo Database")); + items.push(getQuickPickItem("Cosmos DB Gremlin Database")); + items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); + items.push(getQuickPickItem("Cosmos DB Cassandra Table")); + items.push(getQuickPickItem("Cosmos DB SQL Container")); + items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); + items.push(getQuickPickItem("Cosmos DB Table Storage Table")); + items.push(getQuickPickItem("Data Lake Store Account")); + items.push(getQuickPickItem("DNS Record")); + items.push(getQuickPickItem("DNS Zone")); + items.push(getQuickPickItem("Function")); + items.push(getQuickPickItem("KeyVault")); + items.push(getQuickPickItem("KeyVault Secret")); + items.push(getQuickPickItem("Kubernetes Service Cluster")); + items.push(getQuickPickItem("Linux VM Custom Script")); + items.push(getQuickPickItem("Load Balancer External")); + items.push(getQuickPickItem("Load Balancer Internal")); + items.push(getQuickPickItem("Log Analytics Solution")); + items.push(getQuickPickItem("Log Analytics Workspace")); + items.push(getQuickPickItem("Logic App")); + items.push(getQuickPickItem("Logic App Connector")); + items.push(getQuickPickItem("Managed Identity (User Assigned)")); + items.push(getQuickPickItem("Media Services")); + items.push(getQuickPickItem("MySQL Database")); + items.push(getQuickPickItem("Network Interface")); + items.push(getQuickPickItem("Network Security Group")); + items.push(getQuickPickItem("Network Security Group Rule")); + items.push(getQuickPickItem("Public IP Address")); + items.push(getQuickPickItem("Public IP Prefix")); + items.push(getQuickPickItem("Recovery Service Vault")); + items.push(getQuickPickItem("Redis Cache")); + items.push(getQuickPickItem("Route Table")); + items.push(getQuickPickItem("Route Table Route")); + items.push(getQuickPickItem("SQL Database")); + items.push(getQuickPickItem("SQL Database Import")); + items.push(getQuickPickItem("SQL Server")); + items.push(getQuickPickItem("Storage Account")); + items.push(getQuickPickItem("Traffic Manager Profile")); + items.push(getQuickPickItem("Ubuntu Virtual Machine")); + items.push(getQuickPickItem("Virtual Network")); + items.push(getQuickPickItem("VPN Local Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Connection")); + items.push(getQuickPickItem("Web App")); + items.push(getQuickPickItem("Web Deploy for Web App")); + items.push(getQuickPickItem("Windows Virtual Machine")); + items.push(getQuickPickItem("Windows VM Custom Script")); + items.push(getQuickPickItem("Windows VM Diagnostics Extension")); + items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); + return items; +} +export function getQuickPickItem(label: string): vscode.QuickPickItem { + return { label: label }; +} export function getInsertItemType(): QuickPickItem[] { let items: QuickPickItem[] = []; items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); - // items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); + items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); return items; } @@ -59,6 +134,7 @@ export async function insertItem(template: DeploymentTemplate | undefined, sortT await insertParameter(template, textEditor); break; case SortType.Resources: + await insertResource(template, textEditor); break; case SortType.Variables: await insertVariable(template, textEditor); @@ -152,6 +228,28 @@ async function insertFunction(template: DeploymentTemplate, textEditor: vscode.T await insertTextAndSetCursor(textEditor, index, `,\r\n${indentedText}\t`); } } + +async function insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let resources = getTemplateArrayPart(template, templateKeys.resources); + if (!resources) { + return; + } + const resource = await ext.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); + let index = resources.span.endIndex; + let text = "\r\n\t\t"; + if (resources.elements.length > 0) { + let lastIndex = resources.elements.length - 1; + index = resources.elements[lastIndex].span.afterEndIndex; + text = `,${text}`; + } + await insertText(textEditor, index, text); + let pos = textEditor.document.positionAt(index + text.length); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; + await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); +} + async function getFunction(): Promise { const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of function output?' }); let parameters = await getFunctionParameters(); From 774a5dcb6339304795a4537c074f6039800da584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 4 Apr 2020 17:20:49 +0200 Subject: [PATCH 23/61] Added spaces to all 'Insert xxx' changed so that all items in TreeView has the same context as the root Item. --- package.json | 10 +++++----- src/Treeview.ts | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bae70b29b..56a281acf 100644 --- a/package.json +++ b/package.json @@ -220,27 +220,27 @@ }, { "category": "Azure Resource Manager Tools", - "title": "Insert Variable", + "title": "Insert Variable...", "command": "azurerm-vscode-tools.insertVariable" }, { "category": "Azure Resource Manager Tools", - "title": "Insert Function", + "title": "Insert Function...", "command": "azurerm-vscode-tools.insertFunction" }, { "category": "Azure Resource Manager Tools", - "title": "Insert Resource", + "title": "Insert Resource...", "command": "azurerm-vscode-tools.insertResource" }, { "category": "Azure Resource Manager Tools", - "title": "Insert Parameter", + "title": "Insert Parameter...", "command": "azurerm-vscode-tools.insertParameter" }, { "category": "Azure Resource Manager Tools", - "title": "Insert Output", + "title": "Insert Output...", "command": "azurerm-vscode-tools.insertOutput" } ], diff --git a/src/Treeview.ts b/src/Treeview.ts index adfe7b660..7148dc71b 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -299,6 +299,11 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { if (keyNode instanceof Json.StringValue) { return keyNode.unquotedValue; } + } else { + const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start); + if (rootNode instanceof Json.StringValue) { + return rootNode.unquotedValue; + } } return undefined; } From f81d68a94981aabdf6ee9904ee268f23bf13d5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 5 Apr 2020 13:31:40 +0200 Subject: [PATCH 24/61] Refactor insertParameter --- src/insertItem.ts | 65 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 94444713f..7997ec9d1 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -166,34 +166,59 @@ function getTemplatePart(template: DeploymentTemplate, templatePart: string): Js async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let parameters = getTemplateObjectPart(template, templateKeys.parameters); - let startText = parameters?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + if (!parameters) { + return; + } + let startText = '\t\t'; + let index = parameters.span.endIndex; + let endText = "\r\n\t"; + if (parameters.properties.length > 0) { + startText = ","; + index -= 3; + endText = '\t'; + } let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); + let parameter: any = { + type: parameterType.value + }; let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); - let defaultValueText = defaultValue === '' ? '' : `,\r\n\t\t\t"defaultValue": "${defaultValue}"`; + if (defaultValue !== '') { + parameter.defaultValue = defaultValue; + } let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); - let descriptionValueText = descriptionValue === '' ? '' : `,\r\n\t\t\t"metadata": {\r\n\t\t\t\t"description": "${descriptionValue}"\r\n\t\t\t}`; - let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${parameterType.value}"${defaultValueText}${descriptionValueText}\r\n\t\t\}\r\n\t`; - let index = parameters?.span.endIndex; - await insertText(textEditor, index, text); + if (descriptionValue !== '') { + parameter.metadata = { + description: descriptionValue + }; + } + let text = JSON.stringify(parameter, null, '\t'); + let indentedText = indent(`\r\n"${name}": ${text}`, 2); + await insertText(textEditor, index, `${startText}${indentedText}${endText}`); } async function insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let variables = getTemplateObjectPart(template, templateKeys.variables); - let startText = variables?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + if (!variables) { + return; + } + let startText = variables.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of variable?" }); let text = `${startText}"${name}": ""\r\n\t`; - let index = variables?.span.endIndex; + let index = variables.span.endIndex; await insertTextAndSetCursor(textEditor, index, text); } async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let outputs = getTemplateObjectPart(template, templateKeys.outputs); - let startText = outputs?.properties.length === 0 ? '\r\n\t\t' : '\t,'; + if (!outputs) { + return; + } + let startText = outputs.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${outputType.value}",\r\n\t\t\t"value": ""\r\n\t\t\}\r\n\t`; - let index = outputs?.span.endIndex; + let index = outputs.span.endIndex; await insertTextAndSetCursor(textEditor, index, text); } @@ -279,23 +304,21 @@ async function getFunctionParameters(): Promise { return parameters; } -async function insertText(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { - if (index !== undefined) { - await textEditor.edit(builder => { - let i: number = index!; - let pos = textEditor.document.positionAt(i); - builder.insert(pos, text); - }); - } +async function insertText(textEditor: vscode.TextEditor, index: number, text: string): Promise { + await textEditor.edit(builder => { + let i: number = index; + let pos = textEditor.document.positionAt(i); + builder.insert(pos, text); + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); + }); } -async function insertTextAndSetCursor(textEditor: vscode.TextEditor, index: number | undefined, text: string): Promise { +async function insertTextAndSetCursor(textEditor: vscode.TextEditor, index: number, text: string): Promise { await insertText(textEditor, index, text); let cursorPos = text.indexOf('""'); - let pos = textEditor.document.positionAt(index! + cursorPos - 1); + let pos = textEditor.document.positionAt(index + cursorPos - 1); let newSelection = new vscode.Selection(pos, pos); textEditor.selection = newSelection; - textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } /** From d73ffe8d87e6f250b414fba60ad3fafbd8fc95c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 5 Apr 2020 14:29:17 +0200 Subject: [PATCH 25/61] Refactoring of insertItem --- src/insertItem.ts | 87 ++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 7997ec9d1..5f1b4f75d 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -10,6 +10,8 @@ import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; import { SortType } from "./sortTemplate"; +const insertCursorText = '"[]"'; + export class QuickPickItem implements vscode.QuickPickItem { public label: string; public value: T; @@ -165,18 +167,6 @@ function getTemplatePart(template: DeploymentTemplate, templatePart: string): Js } async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let parameters = getTemplateObjectPart(template, templateKeys.parameters); - if (!parameters) { - return; - } - let startText = '\t\t'; - let index = parameters.span.endIndex; - let endText = "\r\n\t"; - if (parameters.properties.length > 0) { - startText = ","; - index -= 3; - endText = '\t'; - } let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); let parameter: any = { @@ -186,40 +176,42 @@ async function insertParameter(template: DeploymentTemplate, textEditor: vscode. if (defaultValue !== '') { parameter.defaultValue = defaultValue; } - let descriptionValue = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); - if (descriptionValue !== '') { + let description = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); + if (description !== '') { parameter.metadata = { - description: descriptionValue + description: description }; } - let text = JSON.stringify(parameter, null, '\t'); + await insertInObject(template, textEditor, templateKeys.parameters, parameter, name); +} + +async function insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { + let templatePart = getTemplateObjectPart(template, part); + if (!templatePart) { + return; + } + let firstItem = templatePart.properties.length === 0; + let startText = firstItem ? '\t\t' : ','; + let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - 3; + let endText = firstItem ? '\r\n\t' : '\t'; + let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; let indentedText = indent(`\r\n"${name}": ${text}`, 2); await insertText(textEditor, index, `${startText}${indentedText}${endText}`); } async function insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let variables = getTemplateObjectPart(template, templateKeys.variables); - if (!variables) { - return; - } - let startText = variables.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of variable?" }); - let text = `${startText}"${name}": ""\r\n\t`; - let index = variables.span.endIndex; - await insertTextAndSetCursor(textEditor, index, text); + await insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); } async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let outputs = getTemplateObjectPart(template, templateKeys.outputs); - if (!outputs) { - return; - } - let startText = outputs.properties.length === 0 ? '\r\n\t\t' : '\t,'; let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); - let text = `${startText}"${name}": \{\r\n\t\t\t"type": "${outputType.value}",\r\n\t\t\t"value": ""\r\n\t\t\}\r\n\t`; - let index = outputs.span.endIndex; - await insertTextAndSetCursor(textEditor, index, text); + let output: any = { + type: outputType.value, + value: insertCursorText + }; + await insertInObject(template, textEditor, templateKeys.outputs, output, name); } async function insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { @@ -246,11 +238,11 @@ async function insertFunction(template: DeploymentTemplate, textEditor: vscode.T }; text = JSON.stringify(namespace, null, '\t'); let indentedText = indent(`\r\n${text}\r\n`, 2); - await insertTextAndSetCursor(textEditor, index, indentedText + '\t'); + await insertText(textEditor, index, indentedText + '\t', true); } else { text = JSON.stringify(functionDef, null, '\t'); let indentedText = indent(`"${functionName}": ${text}\r\n`, 4); - await insertTextAndSetCursor(textEditor, index, `,\r\n${indentedText}\t`); + await insertText(textEditor, index, `,\r\n${indentedText}\t`, true); } } @@ -267,7 +259,7 @@ async function insertResource(template: DeploymentTemplate, textEditor: vscode.T index = resources.elements[lastIndex].span.afterEndIndex; text = `,${text}`; } - await insertText(textEditor, index, text); + await insertText(textEditor, index, text, false); let pos = textEditor.document.positionAt(index + text.length); let newSelection = new vscode.Selection(pos, pos); textEditor.selection = newSelection; @@ -304,21 +296,16 @@ async function getFunctionParameters(): Promise { return parameters; } -async function insertText(textEditor: vscode.TextEditor, index: number, text: string): Promise { - await textEditor.edit(builder => { - let i: number = index; - let pos = textEditor.document.positionAt(i); - builder.insert(pos, text); - textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); - }); -} - -async function insertTextAndSetCursor(textEditor: vscode.TextEditor, index: number, text: string): Promise { - await insertText(textEditor, index, text); - let cursorPos = text.indexOf('""'); - let pos = textEditor.document.positionAt(index + cursorPos - 1); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; +async function insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { + 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.indexOf(insertCursorText) >= 0) { + let insertedText = textEditor.document.getText(new vscode.Range(pos, textEditor.document.positionAt(index + text.length))); + let cursorPos = insertedText.indexOf(insertCursorText); + let pos2 = textEditor.document.positionAt(index + cursorPos + insertCursorText.length / 2); + textEditor.selection = new vscode.Selection(pos2, pos2); + } } /** From 37a0d7b8a33bff3251eea6281fa08d1da154027f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 5 Apr 2020 17:42:32 +0200 Subject: [PATCH 26/61] Refactored Insert Function --- src/insertItem.ts | 108 +++++++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 30 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 5f1b4f75d..f566506dc 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -190,12 +190,17 @@ async function insertInObject(template: DeploymentTemplate, textEditor: vscode.T if (!templatePart) { return; } + await insertInObject2(templatePart, textEditor, data, name); +} + +async function insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { let firstItem = templatePart.properties.length === 0; let startText = firstItem ? '\t\t' : ','; - let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - 3; - let endText = firstItem ? '\r\n\t' : '\t'; + let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - indentLevel - 1; + let tabs = '\t'.repeat(indentLevel - 1); + let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; - let indentedText = indent(`\r\n"${name}": ${text}`, 2); + let indentedText = indent(`\r\n"${name}": ${text}`, indentLevel); await insertText(textEditor, index, `${startText}${indentedText}${endText}`); } @@ -213,37 +218,57 @@ async function insertOutput(template: DeploymentTemplate, textEditor: vscode.Tex }; await insertInObject(template, textEditor, templateKeys.outputs, output, name); } +async function insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { + if (!topLevel) { + return; + } + let functions = [await getFunctionNamespace()]; + await insertInObject2(topLevel, textEditor, functions, "functions", 1); +} + +async function insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { + let namespace = await getFunctionNamespace(); + await insertInArray(functions, textEditor, namespace); +} + +async function insertFunctionMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { + let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); + let functionDef = await getFunction(); + let members: any = {}; + members[functionName] = functionDef; + await insertInObject2(namespace, textEditor, members, 'members', 3); +} + +async function insertFunctionFunction(members: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { + let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); + let functionDef = await getFunction(); + await insertInObject2(members, textEditor, functionDef, functionName, 4); +} async function insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let functions = getTemplateArrayPart(template, templateKeys.functions); - let index: number = 0; - let text: string; - let namespaceName: string = ""; - if (functions?.length === 0) { - namespaceName = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); - index = functions?.span.endIndex; - } else { - let namespace = Json.asObjectValue(functions?.elements[0]); - let members2 = namespace?.getPropertyValue("members"); - index = members2?.span.endIndex! - 4; + if (!functions) { + await insertFunctionTopLevel(template.topLevelValue, textEditor); + return; } - let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); - let functionDef = await getFunction(); - if (namespaceName !== '') { - let members: any = {}; - members[functionName] = functionDef; - let namespace = { - namespace: namespaceName, - members: members - }; - text = JSON.stringify(namespace, null, '\t'); - let indentedText = indent(`\r\n${text}\r\n`, 2); - await insertText(textEditor, index, indentedText + '\t', true); - } else { - text = JSON.stringify(functionDef, null, '\t'); - let indentedText = indent(`"${functionName}": ${text}\r\n`, 4); - await insertText(textEditor, index, `,\r\n${indentedText}\t`, true); + if (functions.length === 0) { + await insertFunctionNamespace(functions, textEditor); + return; + } + let namespace = Json.asObjectValue(functions.elements[0]); + if (!namespace) { + return; + } + let members = namespace.getPropertyValue("members"); + if (!members) { + await insertFunctionMembers(namespace, textEditor); + return; + } + let membersObject = Json.asObjectValue(members); + if (!membersObject) { + return; } + await insertFunctionFunction(membersObject, textEditor); } async function insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { @@ -274,7 +299,7 @@ async function getFunction(): Promise { parameters: parameters, output: { type: outputType.value, - value: "" + value: insertCursorText.replace(/"/g, '') } }; return functionDef; @@ -295,6 +320,29 @@ async function getFunctionParameters(): Promise { } while (parameterName !== ''); return parameters; } +async function insertInArray(templatePart: Json.ArrayValue, textEditor: vscode.TextEditor, data: any): Promise { + let index = templatePart.span.endIndex; + let text = JSON.stringify(data, null, '\t'); + let indentedText = indent(`\r\n${text}\r\n`, 2); + await insertText(textEditor, index, `${indentedText}\t`, true); +} + +async function getFunctionNamespace(): Promise { + let namespaceName = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); + let namespace = { + namespace: namespaceName, + members: await getFunctionMembers() + }; + return namespace; +} + +async function getFunctionMembers(): Promise { + let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); + let functionDef = await getFunction(); + let members: any = {}; + members[functionName] = functionDef; + return members; +} async function insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { let pos = textEditor.document.positionAt(index); From 27b03695b3e62b57dd695ce732386868a7a2ffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 5 Apr 2020 18:13:57 +0200 Subject: [PATCH 27/61] Introduced types and fixed warnings in insertItem.ts --- src/insertItem.ts | 68 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index f566506dc..ded59d3ea 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from "vscode"; +// tslint:disable-next-line:no-duplicate-imports import { commands } from "vscode"; import { Json, templateKeys } from "../extension.bundle"; import { DeploymentTemplate } from "./DeploymentTemplate"; @@ -169,7 +170,7 @@ function getTemplatePart(template: DeploymentTemplate, templatePart: string): Js async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); - let parameter: any = { + let parameter: Parameter = { type: parameterType.value }; let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); @@ -185,6 +186,7 @@ async function insertParameter(template: DeploymentTemplate, textEditor: vscode. await insertInObject(template, textEditor, templateKeys.parameters, parameter, name); } +// tslint:disable-next-line:no-any async function insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { let templatePart = getTemplateObjectPart(template, part); if (!templatePart) { @@ -193,6 +195,7 @@ async function insertInObject(template: DeploymentTemplate, textEditor: vscode.T await insertInObject2(templatePart, textEditor, data, name); } +// tslint:disable-next-line:no-any async function insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { let firstItem = templatePart.properties.length === 0; let startText = firstItem ? '\t\t' : ','; @@ -212,7 +215,7 @@ async function insertVariable(template: DeploymentTemplate, textEditor: vscode.T async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); - let output: any = { + let output: Output = { type: outputType.value, value: insertCursorText }; @@ -234,7 +237,9 @@ async function insertFunctionNamespace(functions: Json.ArrayValue, textEditor: v async function insertFunctionMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); let functionDef = await getFunction(); + // tslint:disable-next-line:no-any let members: any = {}; + // tslint:disable-next-line:no-unsafe-any members[functionName] = functionDef; await insertInObject2(namespace, textEditor, members, 'members', 3); } @@ -292,7 +297,7 @@ async function insertResource(template: DeploymentTemplate, textEditor: vscode.T textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } -async function getFunction(): Promise { +async function getFunction(): Promise { const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of function output?' }); let parameters = await getFunctionParameters(); let functionDef = { @@ -304,7 +309,8 @@ async function getFunction(): Promise { }; return functionDef; } -async function getFunctionParameters(): Promise { + +async function getFunctionParameters(): Promise { let parameterName: string; let parameters = []; do { @@ -320,6 +326,8 @@ async function getFunctionParameters(): Promise { } while (parameterName !== ''); return parameters; } + +// tslint:disable-next-line:no-any async function insertInArray(templatePart: Json.ArrayValue, textEditor: vscode.TextEditor, data: any): Promise { let index = templatePart.span.endIndex; let text = JSON.stringify(data, null, '\t'); @@ -327,7 +335,7 @@ async function insertInArray(templatePart: Json.ArrayValue, textEditor: vscode.T await insertText(textEditor, index, `${indentedText}\t`, true); } -async function getFunctionNamespace(): Promise { +async function getFunctionNamespace(): Promise { let namespaceName = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); let namespace = { namespace: namespaceName, @@ -336,10 +344,13 @@ async function getFunctionNamespace(): Promise { return namespace; } +// tslint:disable-next-line:no-any async function getFunctionMembers(): Promise { let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); let functionDef = await getFunction(); + // tslint:disable-next-line:no-any let members: any = {}; + // tslint:disable-next-line:no-unsafe-any members[functionName] = functionDef; return members; } @@ -358,17 +369,48 @@ async function insertText(textEditor: vscode.TextEditor, index: number, text: st /** * Indents the given string - * @param {string} str The string to be indented. - * @param {number} numOfIndents The amount of indentations to place at the + * @param str The string to be indented. + * @param numOfIndents The amount of indentations to place at the * beginning of each line of the string. - * @param {number=} opt_spacesPerIndent Optional. If specified, this should be - * the number of spaces to be used for each tab that would ordinarily be - * used to indent the text. These amount of spaces will also be used to - * replace any tab characters that already exist within the string. - * @return {string} The new string with each line beginning with the desired + * @return The new string with each line beginning with the desired * amount of indentation. */ -function indent(str: string, numOfIndents: number) { +function indent(str: string, numOfIndents: number): string { + // tslint:disable-next-line:prefer-array-literal str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t')); return str; } + +interface ParameterMetaData { + description: string; +} + +interface Parameter { + // tslint:disable-next-line:no-reserved-keywords + type: string; + defaultValue?: string; + metadata?: ParameterMetaData; +} + +interface Output { + // tslint:disable-next-line:no-reserved-keywords + type: string; + value: string; +} + +interface Function { + parameters: Parameter[]; + output: Output; +} + +interface FunctionParameter { + name: string; + // tslint:disable-next-line:no-reserved-keywords + type: string; +} + +interface FunctionNameSpace { + namespace: string; + // tslint:disable-next-line:no-any + members: any[]; +} From 63c374573ee8e5590900dc36c9a1a62e75478182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 5 Apr 2020 23:35:22 +0200 Subject: [PATCH 28/61] Small fix for output value --- src/insertItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index ded59d3ea..0ca72b5f4 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -217,7 +217,7 @@ async function insertOutput(template: DeploymentTemplate, textEditor: vscode.Tex const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); let output: Output = { type: outputType.value, - value: insertCursorText + value: insertCursorText.replace(/"/g, '') }; await insertInObject(template, textEditor, templateKeys.outputs, output, name); } From 45782d56646f1daa2d97e021ec565d7ab01a6928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Tue, 7 Apr 2020 10:32:18 +0200 Subject: [PATCH 29/61] Started writing unit test for Insert Variable --- src/AzureRMTools.ts | 4 +- src/insertItem.ts | 457 +++++++++++++++-------------- test/functional/insertItem.test.ts | 81 +++++ 3 files changed, 316 insertions(+), 226 deletions(-) create mode 100644 test/functional/insertItem.test.ts diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 58f515e83..9fe4697d9 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -19,7 +19,7 @@ import { Histogram } from "./Histogram"; import * as Hover from './Hover'; import { DefinitionKind } from "./INamedDefinition"; import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; -import { getInsertItemType, insertItem } from "./insertItem"; +import { getInsertItemType, InsertItem } from "./insertItem"; import * as Json from "./JSON"; import * as language from "./Language"; import { reloadSchemas } from "./languageclient/reloadSchemas"; @@ -191,7 +191,7 @@ export class AzureRMTools { documentUri = documentUri || editor?.document.uri; if (editor && documentUri && editor.document.uri.fsPath === documentUri.fsPath) { let deploymentTemplate = this.getDeploymentTemplate(editor.document); - await insertItem(deploymentTemplate, sortType, editor); + await new InsertItem(ext.ui).insertItem(deploymentTemplate, sortType, editor); } } diff --git a/src/insertItem.ts b/src/insertItem.ts index 0ca72b5f4..21ed3650d 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports import { commands } from "vscode"; +import { IAzureUserInput } from "vscode-azureextensionui"; import { Json, templateKeys } from "../extension.bundle"; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; @@ -121,264 +122,272 @@ export function getInsertItemType(): QuickPickItem[] { return items; } -export async function insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { - if (!template) { - return; +export class InsertItem { + private ui: IAzureUserInput; + + constructor(ui: IAzureUserInput) { + this.ui = ui; } - ext.outputChannel.appendLine("Insert item"); - switch (sortType) { - case SortType.Functions: - await insertFunction(template, textEditor); - break; - case SortType.Outputs: - await insertOutput(template, textEditor); - break; - case SortType.Parameters: - await insertParameter(template, textEditor); - break; - case SortType.Resources: - await insertResource(template, textEditor); - break; - case SortType.Variables: - await insertVariable(template, textEditor); - break; - default: - vscode.window.showWarningMessage("Unknown insert item type!"); + + public async insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { + if (!template) { return; + } + ext.outputChannel.appendLine("Insert item"); + switch (sortType) { + case SortType.Functions: + await this.insertFunction(template, textEditor); + break; + case SortType.Outputs: + await this.insertOutput(template, textEditor); + break; + case SortType.Parameters: + await this.insertParameter(template, textEditor); + break; + case SortType.Resources: + await this.insertResource(template, textEditor); + break; + case SortType.Variables: + await this.insertVariable(template, textEditor); + break; + default: + vscode.window.showWarningMessage("Unknown insert item type!"); + return; + } + vscode.window.showInformationMessage("Done inserting item!"); } - vscode.window.showInformationMessage("Done inserting item!"); -} -function getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { - let part = getTemplatePart(template, templatePart); - return Json.asObjectValue(part); -} - -function getTemplateArrayPart(template: DeploymentTemplate, templatePart: string): Json.ArrayValue | undefined { - let part = getTemplatePart(template, templatePart); - return Json.asArrayValue(part); -} - -function getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.Value | undefined { - let rootValue = template.topLevelValue; - if (!rootValue) { - return undefined; + private getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { + let part = this.getTemplatePart(template, templatePart); + return Json.asObjectValue(part); } - return rootValue.getPropertyValue(templatePart); -} -async function insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let name = await ext.ui.showInputBox({ prompt: "Name of parameter?" }); - const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of parameter?' }); - let parameter: Parameter = { - type: parameterType.value - }; - let defaultValue = await ext.ui.showInputBox({ prompt: "Default value? Leave empty for no default value", }); - if (defaultValue !== '') { - parameter.defaultValue = defaultValue; + private getTemplateArrayPart(template: DeploymentTemplate, templatePart: string): Json.ArrayValue | undefined { + let part = this.getTemplatePart(template, templatePart); + return Json.asArrayValue(part); } - let description = await ext.ui.showInputBox({ prompt: "Description? Leave empty for no description", }); - if (description !== '') { - parameter.metadata = { - description: description - }; - } - await insertInObject(template, textEditor, templateKeys.parameters, parameter, name); -} -// tslint:disable-next-line:no-any -async function insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { - let templatePart = getTemplateObjectPart(template, part); - if (!templatePart) { - return; + private getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.Value | undefined { + let rootValue = template.topLevelValue; + if (!rootValue) { + return undefined; + } + return rootValue.getPropertyValue(templatePart); } - await insertInObject2(templatePart, textEditor, data, name); -} - -// tslint:disable-next-line:no-any -async function insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { - let firstItem = templatePart.properties.length === 0; - let startText = firstItem ? '\t\t' : ','; - let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - indentLevel - 1; - let tabs = '\t'.repeat(indentLevel - 1); - let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; - let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; - let indentedText = indent(`\r\n"${name}": ${text}`, indentLevel); - await insertText(textEditor, index, `${startText}${indentedText}${endText}`); -} - -async function insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let name = await ext.ui.showInputBox({ prompt: "Name of variable?" }); - await insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); -} -async function insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let name = await ext.ui.showInputBox({ prompt: "Name of output?" }); - const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of output?' }); - let output: Output = { - type: outputType.value, - value: insertCursorText.replace(/"/g, '') - }; - await insertInObject(template, textEditor, templateKeys.outputs, output, name); -} -async function insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { - if (!topLevel) { - return; + private async insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): 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); } - let functions = [await getFunctionNamespace()]; - await insertInObject2(topLevel, textEditor, functions, "functions", 1); -} -async function insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { - let namespace = await getFunctionNamespace(); - await insertInArray(functions, textEditor, namespace); -} - -async function insertFunctionMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { - let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); - let functionDef = await getFunction(); // tslint:disable-next-line:no-any - let members: any = {}; - // tslint:disable-next-line:no-unsafe-any - members[functionName] = functionDef; - await insertInObject2(namespace, textEditor, members, 'members', 3); -} - -async function insertFunctionFunction(members: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { - let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); - let functionDef = await getFunction(); - await insertInObject2(members, textEditor, functionDef, functionName, 4); -} + private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { + let templatePart = this.getTemplateObjectPart(template, part); + if (!templatePart) { + return; + } + await this.insertInObject2(templatePart, textEditor, data, name); + } -async function insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let functions = getTemplateArrayPart(template, templateKeys.functions); - if (!functions) { - await insertFunctionTopLevel(template.topLevelValue, textEditor); - return; + // tslint:disable-next-line:no-any + private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { + let firstItem = templatePart.properties.length === 0; + let startText = firstItem ? '\t\t' : ','; + let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - indentLevel - 1; + let tabs = '\t'.repeat(indentLevel - 1); + let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; + let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; + let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel); + await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); } - if (functions.length === 0) { - await insertFunctionNamespace(functions, textEditor); - return; + + private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let name = await this.ui.showInputBox({ prompt: "Name of variable?" }); + await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); } - let namespace = Json.asObjectValue(functions.elements[0]); - if (!namespace) { - return; + + private async insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): 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); } - let members = namespace.getPropertyValue("members"); - if (!members) { - await insertFunctionMembers(namespace, textEditor); - return; + private async insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { + if (!topLevel) { + return; + } + let functions = [await this.getFunctionNamespace()]; + await this.insertInObject2(topLevel, textEditor, functions, "functions", 1); } - let membersObject = Json.asObjectValue(members); - if (!membersObject) { - return; + + private async insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { + let namespace = await this.getFunctionNamespace(); + await this.insertInArray(functions, textEditor, namespace); } - await insertFunctionFunction(membersObject, textEditor); -} -async function insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { - let resources = getTemplateArrayPart(template, templateKeys.resources); - if (!resources) { - return; + private async insertFunctionMembers(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.insertInObject2(namespace, textEditor, members, 'members', 3); } - const resource = await ext.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); - let index = resources.span.endIndex; - let text = "\r\n\t\t"; - if (resources.elements.length > 0) { - let lastIndex = resources.elements.length - 1; - index = resources.elements[lastIndex].span.afterEndIndex; - text = `,${text}`; + + private async insertFunctionFunction(members: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { + let functionName = await this.ui.showInputBox({ prompt: "Name of function?" }); + let functionDef = await this.getFunction(); + await this.insertInObject2(members, textEditor, functionDef, functionName, 4); } - await insertText(textEditor, index, text, false); - let pos = textEditor.document.positionAt(index + text.length); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; - await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); - textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); -} -async function getFunction(): Promise { - const outputType = await ext.ui.showQuickPick(getItemType(), { placeHolder: 'Type of function output?' }); - let parameters = await getFunctionParameters(); - let functionDef = { - parameters: parameters, - output: { - type: outputType.value, - value: insertCursorText.replace(/"/g, '') + private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let functions = this.getTemplateArrayPart(template, templateKeys.functions); + if (!functions) { + await this.insertFunctionTopLevel(template.topLevelValue, textEditor); + return; } - }; - return functionDef; -} + if (functions.length === 0) { + await this.insertFunctionNamespace(functions, textEditor); + return; + } + let namespace = Json.asObjectValue(functions.elements[0]); + if (!namespace) { + return; + } + let members = namespace.getPropertyValue("members"); + if (!members) { + await this.insertFunctionMembers(namespace, textEditor); + return; + } + let membersObject = Json.asObjectValue(members); + if (!membersObject) { + return; + } + await this.insertFunctionFunction(membersObject, textEditor); + } -async function getFunctionParameters(): Promise { - let parameterName: string; - let parameters = []; - do { - parameterName = await ext.ui.showInputBox({ prompt: "Name of parameter? Leave empty for no more parameters" }); - if (parameterName !== '') { - const parameterType = await ext.ui.showQuickPick(getItemType(), { placeHolder: `Type of parameter ${parameterName}?` }); - parameters.push({ - name: parameterName, - type: parameterType.value - }); + private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + let resources = this.getTemplateArrayPart(template, templateKeys.resources); + if (!resources) { + return; + } + const resource = await this.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); + let index = resources.span.endIndex; + let text = "\r\n\t\t"; + if (resources.elements.length > 0) { + let lastIndex = resources.elements.length - 1; + index = resources.elements[lastIndex].span.afterEndIndex; + text = `,${text}`; } + await this.insertText(textEditor, index, text, false); + let pos = textEditor.document.positionAt(index + text.length); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; + await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); + textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); + } - } while (parameterName !== ''); - return parameters; -} + 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.replace(/"/g, '') + } + }; + return functionDef; + } -// tslint:disable-next-line:no-any -async function insertInArray(templatePart: Json.ArrayValue, textEditor: vscode.TextEditor, data: any): Promise { - let index = templatePart.span.endIndex; - let text = JSON.stringify(data, null, '\t'); - let indentedText = indent(`\r\n${text}\r\n`, 2); - await insertText(textEditor, index, `${indentedText}\t`, true); -} + 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; + } -async function getFunctionNamespace(): Promise { - let namespaceName = await ext.ui.showInputBox({ prompt: "Name of namespace?" }); - let namespace = { - namespace: namespaceName, - members: await getFunctionMembers() - }; - return namespace; -} + // 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`, true); + } + + 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 -async function getFunctionMembers(): Promise { - let functionName = await ext.ui.showInputBox({ prompt: "Name of function?" }); - let functionDef = await getFunction(); // tslint:disable-next-line:no-any - let members: any = {}; - // tslint:disable-next-line:no-unsafe-any - members[functionName] = functionDef; - return members; -} + 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; + } -async function insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { - 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.indexOf(insertCursorText) >= 0) { - let insertedText = textEditor.document.getText(new vscode.Range(pos, textEditor.document.positionAt(index + text.length))); - let cursorPos = insertedText.indexOf(insertCursorText); - let pos2 = textEditor.document.positionAt(index + cursorPos + insertCursorText.length / 2); - textEditor.selection = new vscode.Selection(pos2, pos2); + private async insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { + 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.indexOf(insertCursorText) >= 0) { + let insertedText = textEditor.document.getText(new vscode.Range(pos, textEditor.document.positionAt(index + text.length))); + let cursorPos = insertedText.indexOf(insertCursorText); + let pos2 = textEditor.document.positionAt(index + cursorPos + insertCursorText.length / 2); + textEditor.selection = new vscode.Selection(pos2, pos2); + } } -} -/** - * Indents the given string - * @param str The string to be indented. - * @param numOfIndents 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. - */ -function indent(str: string, numOfIndents: number): string { - // tslint:disable-next-line:prefer-array-literal - str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t')); - return str; + /** + * Indents the given string + * @param str The string to be indented. + * @param numOfIndents 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, numOfIndents: number): string { + // tslint:disable-next-line:prefer-array-literal + str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t')); + return str; + } } interface ParameterMetaData { diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts new file mode 100644 index 000000000..6a704aa33 --- /dev/null +++ b/test/functional/insertItem.test.ts @@ -0,0 +1,81 @@ +// ---------------------------------------------------------------------------- +// 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"; +import { window, workspace } from "vscode"; +import { IAzureUserInput } from 'vscode-azureextensionui'; +import { DeploymentTemplate } from '../../src/DeploymentTemplate'; +import { InsertItem } from '../../src/insertItem'; +import { SortType } from '../../src/sortTemplate'; +import { getTempFilePath } from "../support/getTempFilePath"; + +suite("InsertItem", async (): Promise => { + const parameterCommand = 'azurerm-vscode-tools.insertParameter'; + const variableCommand = 'azurerm-vscode-tools.insertVariable'; + const command = 'azurerm-vscode-tools.insertItem'; + const resourceCommand = 'azurerm-vscode-tools.insertResource'; + const outputCommand = 'azurerm-vscode-tools.insertOutput'; + const functionCommand = 'azurerm-vscode-tools.insertFunction'; + + const emptyTemplate = `{ + "variables": {} + }`; + + async function testInsertItem(command: string, template: String, expected: String): Promise { + const tempPath = getTempFilePath(`insertItem`, '.azrm'); + + fse.writeFileSync(tempPath, template); + + let doc = await workspace.openTextDocument(tempPath); + await window.showTextDocument(doc); + + // InsertItem + let ui = new MockUserInput(); + let insertItem = new InsertItem(ui); + let document = window.activeTextEditor!.document; + let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri.toString()); + await insertItem.insertItem(deploymentTemplate, SortType.Variables, window.activeTextEditor!); + // await commands.executeCommand(command, null, null, null, { hello: 'World!' }); + + const docTextAfterInsertion = window.activeTextEditor!.document.getText(); + assert.deepStrictEqual(docTextAfterInsertion, expected); + } + + test("Variables", async () => { + await testInsertItem( + command, emptyTemplate, + `{ + "variables": { + "variable1": "[]" + } + }`); + }); +}); + +class MockUserInput implements IAzureUserInput { + public async showQuickPick(items: T[] | Thenable, options: import("vscode-azureextensionui").IAzureQuickPickOptions): Promise { + let result = await items; + return result[0]; + } + + public async showInputBox(options: vscode.InputBoxOptions): Promise { + return "Hello World"; + } + + 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")]; + } +} From 8cf95f16d4cbfecd49ed16790f4b8aa2d0af5ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 8 Apr 2020 00:28:19 +0200 Subject: [PATCH 30/61] First passing test for Insert Variable --- extension.bundle.ts | 2 ++ src/insertItem.ts | 2 +- test/functional/insertItem.test.ts | 41 +++++++++++++++++------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index cf910d4a4..6a45fd512 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -44,6 +44,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"; @@ -52,6 +53,7 @@ export { mayBeMatchingParameterFile } from "./src/parameterFiles"; export { IReferenceSite, PositionContext } from "./src/PositionContext"; export { ReferenceList } from "./src/ReferenceList"; export { containsArmSchema, getPreferredSchema, isArmSchema } from './src/schemas'; +export { SortType } from "./src/sortTemplate"; export * from "./src/survey"; export { ScopeContext, TemplateScope } from "./src/TemplateScope"; export { FunctionSignatureHelp } from "./src/TLE"; diff --git a/src/insertItem.ts b/src/insertItem.ts index 21ed3650d..a3669be1a 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -206,7 +206,7 @@ export class InsertItem { // tslint:disable-next-line:no-any private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { let firstItem = templatePart.properties.length === 0; - let startText = firstItem ? '\t\t' : ','; + let startText = firstItem ? '' : ','; let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - indentLevel - 1; let tabs = '\t'.repeat(indentLevel - 1); let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 6a704aa33..b9eb3a369 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -13,22 +13,21 @@ import * as fse from 'fs-extra'; import * as vscode from "vscode"; import { window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; -import { DeploymentTemplate } from '../../src/DeploymentTemplate'; -import { InsertItem } from '../../src/insertItem'; -import { SortType } from '../../src/sortTemplate'; +import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { - const parameterCommand = 'azurerm-vscode-tools.insertParameter'; - const variableCommand = 'azurerm-vscode-tools.insertVariable'; + // const parameterCommand = 'azurerm-vscode-tools.insertParameter'; + // const variableCommand = 'azurerm-vscode-tools.insertVariable'; const command = 'azurerm-vscode-tools.insertItem'; - const resourceCommand = 'azurerm-vscode-tools.insertResource'; - const outputCommand = 'azurerm-vscode-tools.insertOutput'; - const functionCommand = 'azurerm-vscode-tools.insertFunction'; + // const resourceCommand = 'azurerm-vscode-tools.insertResource'; + // const outputCommand = 'azurerm-vscode-tools.insertOutput'; + // const functionCommand = 'azurerm-vscode-tools.insertFunction'; - const emptyTemplate = `{ - "variables": {} - }`; + const emptyTemplate = + `{ + "variables": {} +}`; async function testInsertItem(command: string, template: String, expected: String): Promise { const tempPath = getTempFilePath(`insertItem`, '.azrm'); @@ -39,7 +38,7 @@ suite("InsertItem", async (): Promise => { await window.showTextDocument(doc); // InsertItem - let ui = new MockUserInput(); + let ui = new MockUserInput(["variable1"]); let insertItem = new InsertItem(ui); let document = window.activeTextEditor!.document; let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri.toString()); @@ -47,28 +46,34 @@ suite("InsertItem", async (): Promise => { // await commands.executeCommand(command, null, null, null, { hello: 'World!' }); const docTextAfterInsertion = window.activeTextEditor!.document.getText(); - assert.deepStrictEqual(docTextAfterInsertion, expected); + // assert.deepStrictEqual(docTextAfterInsertion, expected); + assert.equal(docTextAfterInsertion.replace(/\t/g, ' '), expected.replace(/\t/g, ' ')); } test("Variables", async () => { await testInsertItem( command, emptyTemplate, `{ - "variables": { - "variable1": "[]" - } - }`); + "variables": { + "variable1": "[]" + } +}` + ); }); }); class MockUserInput implements IAzureUserInput { + private showInputBoxTexts: string[] = []; + constructor(showInputBox: string[]) { + this.showInputBoxTexts = showInputBox; + } public async showQuickPick(items: T[] | Thenable, options: import("vscode-azureextensionui").IAzureQuickPickOptions): Promise { let result = await items; return result[0]; } public async showInputBox(options: vscode.InputBoxOptions): Promise { - return "Hello World"; + return this.showInputBoxTexts.pop()!; } public async showWarningMessage(message: string, options: import("vscode-azureextensionui").IAzureMessageOptions, ...items: T[]): Promise { From 3b1d2ee6cfda5a29feebdbe88ba39dda6a43ec59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 8 Apr 2020 11:52:03 +0200 Subject: [PATCH 31/61] Testing insert variable with different settings --- src/insertItem.ts | 12 ++++ test/functional/insertItem.test.ts | 98 ++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index a3669be1a..1c69fc04a 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -212,6 +212,7 @@ export class InsertItem { let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel); + await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); } @@ -364,6 +365,7 @@ export class InsertItem { } private async insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): 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); @@ -375,6 +377,16 @@ export class InsertItem { } } + private formatText(text: string, textEditor: vscode.TextEditor): string { + if (textEditor.options.insertSpaces === true) { + text = text.replace(/\t/g, ' '.repeat(Number(textEditor.options.tabSize))); + } + if (textEditor.document.eol === vscode.EndOfLine.LF) { + text = text.replace(/\r\n/g, '\n'); + } + return text; + } + /** * Indents the given string * @param str The string to be indented. diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index b9eb3a369..ff8312613 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -7,7 +7,6 @@ // 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"; @@ -16,46 +15,95 @@ import { IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; +let previousSettings = { + insertSpaces: undefined, + tabSize: undefined, + eol: undefined, +}; + +namespace configKeys { + export const editor = 'editor'; + export const insertSpaces = 'insertSpaces'; + export const tabSize = 'tabSize'; +} + suite("InsertItem", async (): Promise => { - // const parameterCommand = 'azurerm-vscode-tools.insertParameter'; - // const variableCommand = 'azurerm-vscode-tools.insertVariable'; - const command = 'azurerm-vscode-tools.insertItem'; - // const resourceCommand = 'azurerm-vscode-tools.insertResource'; - // const outputCommand = 'azurerm-vscode-tools.insertOutput'; - // const functionCommand = 'azurerm-vscode-tools.insertFunction'; + function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor) { + let spaces = textEditor.options.insertSpaces; + let tabs = textEditor.options.tabSize; + let eol = textEditor.document.eol; + if (spaces === true) { + expected = expected.replace(/\t/g, ' '.repeat(Number(tabs))); + } + if (eol === vscode.EndOfLine.CRLF) { + expected = expected.replace(/\n/g, '\r\n'); + } + assert.equal(actual.replace(/\t/g, ' '), expected); + } const emptyTemplate = `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], "variables": {} }`; - async function testInsertItem(command: string, template: String, expected: String): Promise { - const tempPath = getTempFilePath(`insertItem`, '.azrm'); + async function testInsertItem(template: String, expected: String): Promise { + test("Tabs CRLF", async () => { + await testInsertItemWithSettings(template, expected, false, 4, true); + }); + test("Spaces CRLF", async () => { + await testInsertItemWithSettings(template, expected, true, 4, true); + }); + test("Spaces LF", async () => { + await testInsertItemWithSettings(template, expected, true, 4, false); + }); + test("Tabs LF", async () => { + await testInsertItemWithSettings(template, expected, false, 4, false); + }); + } + async function testInsertItemWithSettings(template: String, expected: String, insertSpaces: boolean, tabSize: number, eolAsCRLF: boolean): Promise { + let config = vscode.workspace.getConfiguration(configKeys.editor); + config.update(configKeys.insertSpaces, insertSpaces, vscode.ConfigurationTarget.Global); + config.update(configKeys.tabSize, tabSize, vscode.ConfigurationTarget.Global); + if (eolAsCRLF) { + template = template.replace(/\n/g, '\r\n'); + } + const tempPath = getTempFilePath(`insertItem`, '.azrm'); fse.writeFileSync(tempPath, template); - - let doc = await workspace.openTextDocument(tempPath); - await window.showTextDocument(doc); - - // InsertItem + let document = await workspace.openTextDocument(tempPath); + let textEditor = await window.showTextDocument(document); let ui = new MockUserInput(["variable1"]); let insertItem = new InsertItem(ui); - let document = window.activeTextEditor!.document; let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri.toString()); - await insertItem.insertItem(deploymentTemplate, SortType.Variables, window.activeTextEditor!); - // await commands.executeCommand(command, null, null, null, { hello: 'World!' }); - - const docTextAfterInsertion = window.activeTextEditor!.document.getText(); - // assert.deepStrictEqual(docTextAfterInsertion, expected); - assert.equal(docTextAfterInsertion.replace(/\t/g, ' '), expected.replace(/\t/g, ' ')); + await insertItem.insertItem(deploymentTemplate, SortType.Variables, textEditor); + await textEditor.edit(builder => builder.insert(textEditor.selection.active, "resourceGroup()")); + const docTextAfterInsertion = document.getText(); + assertTemplate(docTextAfterInsertion, expected, textEditor); } - test("Variables", async () => { - await testInsertItem( - command, emptyTemplate, + // beforeEach(() => { + // let config = vscode.workspace.getConfiguration(configKeys.editor); + // previousSettings.insertSpaces = config.get(configKeys.insertSpaces); + // previousSettings.tabSize = config.get(configKeys.tabSize); + // }); + + // afterEach(() => { + // let config = vscode.workspace.getConfiguration(configKeys.editor); + // config.update(configKeys.insertSpaces, previousSettings.insertSpaces, vscode.ConfigurationTarget.Global); + // config.update(configKeys.tabSize, previousSettings.tabSize, vscode.ConfigurationTarget.Global); + // }); + + suite("Variables", async () => { + await testInsertItem(emptyTemplate, `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], "variables": { - "variable1": "[]" + "variable1": "[resourceGroup()]" } }` ); From f1662ff447a5fd5b217988adbbefaee5b6e0dcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Wed, 8 Apr 2020 11:55:33 +0200 Subject: [PATCH 32/61] Minimized template to test --- test/functional/insertItem.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index ff8312613..2358dacfe 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -43,9 +43,6 @@ suite("InsertItem", async (): Promise => { const emptyTemplate = `{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], "variables": {} }`; @@ -99,9 +96,6 @@ suite("InsertItem", async (): Promise => { suite("Variables", async () => { await testInsertItem(emptyTemplate, `{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], "variables": { "variable1": "[resourceGroup()]" } From f7b7324bba4d89e232a1ed4107cc04165e53e908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Thu, 9 Apr 2020 11:21:37 +0200 Subject: [PATCH 33/61] Improved tests for Insert Variable --- src/insertItem.ts | 2 ++ test/functional/insertItem.test.ts | 55 ++++++++++-------------------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 1c69fc04a..0da0d4844 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -380,6 +380,8 @@ export class InsertItem { 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(/ /g, '\t'); } if (textEditor.document.eol === vscode.EndOfLine.LF) { text = text.replace(/\r\n/g, '\n'); diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 2358dacfe..7b0360079 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -15,30 +15,17 @@ import { IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; -let previousSettings = { - insertSpaces: undefined, - tabSize: undefined, - eol: undefined, -}; - -namespace configKeys { - export const editor = 'editor'; - export const insertSpaces = 'insertSpaces'; - export const tabSize = 'tabSize'; -} - suite("InsertItem", async (): Promise => { - function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor) { - let spaces = textEditor.options.insertSpaces; - let tabs = textEditor.options.tabSize; - let eol = textEditor.document.eol; - if (spaces === true) { - expected = expected.replace(/\t/g, ' '.repeat(Number(tabs))); + function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor): void { + if (textEditor.options.insertSpaces === true) { + expected = expected.replace(/ /g, ' '.repeat(Number(textEditor.options.tabSize))); + } else { + expected = expected.replace(/ /g, '\t'); } - if (eol === vscode.EndOfLine.CRLF) { + if (textEditor.document.eol === vscode.EndOfLine.CRLF) { expected = expected.replace(/\n/g, '\r\n'); } - assert.equal(actual.replace(/\t/g, ' '), expected); + assert.equal(actual, expected); } const emptyTemplate = @@ -46,28 +33,34 @@ suite("InsertItem", async (): Promise => { "variables": {} }`; - async function testInsertItem(template: String, expected: String): Promise { + async function testInsertItem(template: string, expected: String): Promise { test("Tabs CRLF", async () => { await testInsertItemWithSettings(template, expected, false, 4, true); }); test("Spaces CRLF", async () => { await testInsertItemWithSettings(template, expected, true, 4, true); }); + test("Spaces (2) CRLF", async () => { + await testInsertItemWithSettings(template, expected, true, 2, true); + }); test("Spaces LF", async () => { await testInsertItemWithSettings(template, expected, true, 4, false); }); test("Tabs LF", async () => { await testInsertItemWithSettings(template, expected, false, 4, false); }); + test("Spaces (2) LF", async () => { + await testInsertItemWithSettings(template, expected, true, 2, false); + }); } - async function testInsertItemWithSettings(template: String, expected: String, insertSpaces: boolean, tabSize: number, eolAsCRLF: boolean): Promise { - let config = vscode.workspace.getConfiguration(configKeys.editor); - config.update(configKeys.insertSpaces, insertSpaces, vscode.ConfigurationTarget.Global); - config.update(configKeys.tabSize, tabSize, vscode.ConfigurationTarget.Global); + async function testInsertItemWithSettings(template: string, expected: String, insertSpaces: boolean, tabSize: number, eolAsCRLF: boolean): Promise { if (eolAsCRLF) { template = template.replace(/\n/g, '\r\n'); } + if (insertSpaces && tabSize != 4) { + template = template.replace(/ /g, ' '.repeat(tabSize)); + } const tempPath = getTempFilePath(`insertItem`, '.azrm'); fse.writeFileSync(tempPath, template); let document = await workspace.openTextDocument(tempPath); @@ -81,18 +74,6 @@ suite("InsertItem", async (): Promise => { assertTemplate(docTextAfterInsertion, expected, textEditor); } - // beforeEach(() => { - // let config = vscode.workspace.getConfiguration(configKeys.editor); - // previousSettings.insertSpaces = config.get(configKeys.insertSpaces); - // previousSettings.tabSize = config.get(configKeys.tabSize); - // }); - - // afterEach(() => { - // let config = vscode.workspace.getConfiguration(configKeys.editor); - // config.update(configKeys.insertSpaces, previousSettings.insertSpaces, vscode.ConfigurationTarget.Global); - // config.update(configKeys.tabSize, previousSettings.tabSize, vscode.ConfigurationTarget.Global); - // }); - suite("Variables", async () => { await testInsertItem(emptyTemplate, `{ From 1e33ce9f9f907be2a2517ffa054013e7bf58d506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Thu, 9 Apr 2020 20:17:50 +0200 Subject: [PATCH 34/61] Added tests for Insert Output --- src/insertItem.ts | 4 +- test/functional/insertItem.test.ts | 111 +++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 0da0d4844..c2d020785 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -207,9 +207,9 @@ export class InsertItem { private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { let firstItem = templatePart.properties.length === 0; let startText = firstItem ? '' : ','; - let index = firstItem ? templatePart.span.endIndex : templatePart.span.endIndex - indentLevel - 1; + let index = firstItem ? templatePart.span.endIndex : templatePart.properties[templatePart.properties.length - 1].span.afterEndIndex; let tabs = '\t'.repeat(indentLevel - 1); - let endText = firstItem ? `\r\n${tabs}` : `${tabs}`; + let endText = firstItem ? `\r\n${tabs}` : ``; let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel); diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 7b0360079..9f76901c3 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -28,33 +28,28 @@ suite("InsertItem", async (): Promise => { assert.equal(actual, expected); } - const emptyTemplate = - `{ - "variables": {} -}`; - - async function testInsertItem(template: string, expected: String): Promise { + async function testInsertItem(template: string, expected: String, action: (insertItem: InsertItem, deploymentTemplate: DeploymentTemplate, textEditor: vscode.TextEditor) => Promise, showInputBox: string[], textToInsert: string = ''): Promise { test("Tabs CRLF", async () => { - await testInsertItemWithSettings(template, expected, false, 4, true); + await testInsertItemWithSettings(template, expected, false, 4, true, action, showInputBox, textToInsert); }); test("Spaces CRLF", async () => { - await testInsertItemWithSettings(template, expected, true, 4, true); + await testInsertItemWithSettings(template, expected, true, 4, true, action, showInputBox, textToInsert); }); test("Spaces (2) CRLF", async () => { - await testInsertItemWithSettings(template, expected, true, 2, true); + await testInsertItemWithSettings(template, expected, true, 2, true, action, showInputBox, textToInsert); }); test("Spaces LF", async () => { - await testInsertItemWithSettings(template, expected, true, 4, false); + await testInsertItemWithSettings(template, expected, true, 4, false, action, showInputBox, textToInsert); }); test("Tabs LF", async () => { - await testInsertItemWithSettings(template, expected, false, 4, false); + await testInsertItemWithSettings(template, expected, false, 4, false, action, showInputBox, textToInsert); }); test("Spaces (2) LF", async () => { - await testInsertItemWithSettings(template, expected, true, 2, false); + await testInsertItemWithSettings(template, expected, true, 2, false, action, showInputBox, textToInsert); }); } - async function testInsertItemWithSettings(template: string, expected: String, insertSpaces: boolean, tabSize: number, eolAsCRLF: boolean): Promise { + 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 = ''): Promise { if (eolAsCRLF) { template = template.replace(/\n/g, '\r\n'); } @@ -65,30 +60,106 @@ suite("InsertItem", async (): Promise => { fse.writeFileSync(tempPath, template); let document = await workspace.openTextDocument(tempPath); let textEditor = await window.showTextDocument(document); - let ui = new MockUserInput(["variable1"]); + let ui = new MockUserInput(showInputBox); let insertItem = new InsertItem(ui); let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri.toString()); - await insertItem.insertItem(deploymentTemplate, SortType.Variables, textEditor); - await textEditor.edit(builder => builder.insert(textEditor.selection.active, "resourceGroup()")); + await action(insertItem, deploymentTemplate, textEditor); + await textEditor.edit(builder => builder.insert(textEditor.selection.active, textToInsert)); const docTextAfterInsertion = document.getText(); assertTemplate(docTextAfterInsertion, expected, textEditor); } suite("Variables", async () => { - await testInsertItem(emptyTemplate, + 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 testInsertItem(emptyTemplate, oneVariableTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable1"], 'resourceGroup()'); + }); + suite("Insert one more variable", async () => { + await testInsertItem(oneVariableTemplate, twoVariablesTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable2"], 'resourceGroup()'); + }); + suite("Insert even one more variable", async () => { + await testInsertItem(twoVariablesTemplate, threeVariablesTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable3"], 'resourceGroup()'); + }); + }); + + 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": "string", + "value": "[resourceGroup()]" + } + } +}`; + suite("Insert one output", async () => { + await testInsertItem(emptyTemplate, oneOutputTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output1"], 'resourceGroup()'); + }); + suite("Insert one more variable", async () => { + await testInsertItem(oneOutputTemplate, twoOutputsTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output2"], 'resourceGroup()'); + }); + suite("Insert even one more variable", async () => { + await testInsertItem(twoOutputsTemplate, threeOutputsTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output3"], 'resourceGroup()'); + }); }); }); class MockUserInput implements IAzureUserInput { private showInputBoxTexts: string[] = []; constructor(showInputBox: string[]) { - this.showInputBoxTexts = showInputBox; + this.showInputBoxTexts = Object.assign([], showInputBox); } public async showQuickPick(items: T[] | Thenable, options: import("vscode-azureextensionui").IAzureQuickPickOptions): Promise { let result = await items; From a5dd788a2bbccbe36a3c17a803f404c29c700c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 10 Apr 2020 01:42:34 +0200 Subject: [PATCH 35/61] Added tests that variables and outputs can be added to {} --- src/insertItem.ts | 15 ++++++++++++--- test/functional/insertItem.test.ts | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index c2d020785..8159bf4cf 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -12,7 +12,7 @@ import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; import { SortType } from "./sortTemplate"; -const insertCursorText = '"[]"'; +const insertCursorText = '[]'; export class QuickPickItem implements vscode.QuickPickItem { public label: string; @@ -198,6 +198,15 @@ export class InsertItem { private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { let templatePart = this.getTemplateObjectPart(template, part); if (!templatePart) { + let topLevel = template.topLevelValue; + if (!topLevel) { + return; + } + // tslint:disable-next-line:no-any + let subPart: any = {}; + // tslint:disable-next-line:no-unsafe-any + subPart[name] = data; + await this.insertInObject2(topLevel, textEditor, subPart, part, 1); return; } await this.insertInObject2(templatePart, textEditor, data, name); @@ -210,7 +219,7 @@ export class InsertItem { let index = firstItem ? templatePart.span.endIndex : templatePart.properties[templatePart.properties.length - 1].span.afterEndIndex; let tabs = '\t'.repeat(indentLevel - 1); let endText = firstItem ? `\r\n${tabs}` : ``; - let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : data; + let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : `"${data}"`; let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel); await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); @@ -381,7 +390,7 @@ export class InsertItem { if (textEditor.options.insertSpaces === true) { text = text.replace(/\t/g, ' '.repeat(Number(textEditor.options.tabSize))); } else { - text = text.replace(/ /g, '\t'); + text = text.replace(/ {4}/g, '\t'); } if (textEditor.document.eol === vscode.EndOfLine.LF) { text = text.replace(/\r\n/g, '\n'); diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 9f76901c3..823a0f414 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -10,6 +10,7 @@ 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 { IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; @@ -18,9 +19,9 @@ import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor): void { if (textEditor.options.insertSpaces === true) { - expected = expected.replace(/ /g, ' '.repeat(Number(textEditor.options.tabSize))); + expected = expected.replace(/ {4}/g, ' '.repeat(Number(textEditor.options.tabSize))); } else { - expected = expected.replace(/ /g, '\t'); + expected = expected.replace(/ {4}/g, '\t'); } if (textEditor.document.eol === vscode.EndOfLine.CRLF) { expected = expected.replace(/\n/g, '\r\n'); @@ -53,8 +54,8 @@ suite("InsertItem", async (): Promise => { if (eolAsCRLF) { template = template.replace(/\n/g, '\r\n'); } - if (insertSpaces && tabSize != 4) { - template = template.replace(/ /g, ' '.repeat(tabSize)); + if (insertSpaces && tabSize !== 4) { + template = template.replace(/ {4}/g, ' '.repeat(tabSize)); } const tempPath = getTempFilePath(`insertItem`, '.azrm'); fse.writeFileSync(tempPath, template); @@ -69,6 +70,9 @@ suite("InsertItem", async (): Promise => { assertTemplate(docTextAfterInsertion, expected, textEditor); } + const totallyEmptyTemplate = + `{}`; + suite("Variables", async () => { const emptyTemplate = `{ @@ -101,6 +105,10 @@ suite("InsertItem", async (): Promise => { suite("Insert even one more variable", async () => { await testInsertItem(twoVariablesTemplate, threeVariablesTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable3"], 'resourceGroup()'); }); + + suite("Insert one variable in totally empty template", async () => { + await testInsertItem(totallyEmptyTemplate, oneVariableTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable1"], 'resourceGroup()'); + }); }); suite("Outputs", async () => { @@ -153,6 +161,9 @@ suite("InsertItem", async (): Promise => { suite("Insert even one more variable", async () => { await testInsertItem(twoOutputsTemplate, threeOutputsTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output3"], 'resourceGroup()'); }); + suite("Insert one output in totally empty template", async () => { + await testInsertItem(totallyEmptyTemplate, oneOutputTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output1"], 'resourceGroup()'); + }); }); }); From a8383ae9d1b5041d752be7277402d70def87673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 10 Apr 2020 17:00:20 +0200 Subject: [PATCH 36/61] Added tests for parameters --- test/functional/insertItem.test.ts | 91 ++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 823a0f414..75a37a4c6 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -73,6 +73,10 @@ suite("InsertItem", async (): Promise => { const totallyEmptyTemplate = `{}`; + async function doTestInsertItem(startTemplate: string, expectedTemplate: string, type: SortType, showInputBox: string[] = [], textToInsert: string = ''): Promise { + await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, type, editor), showInputBox, textToInsert); + } + suite("Variables", async () => { const emptyTemplate = `{ @@ -97,20 +101,83 @@ suite("InsertItem", async (): Promise => { } }`; suite("Insert one variable", async () => { - await testInsertItem(emptyTemplate, oneVariableTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable1"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], 'resourceGroup()'); }); suite("Insert one more variable", async () => { - await testInsertItem(oneVariableTemplate, twoVariablesTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable2"], 'resourceGroup()'); + await doTestInsertItem(oneVariableTemplate, twoVariablesTemplate, SortType.Variables, ["variable2"], 'resourceGroup()'); }); suite("Insert even one more variable", async () => { - await testInsertItem(twoVariablesTemplate, threeVariablesTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable3"], 'resourceGroup()'); + await doTestInsertItem(twoVariablesTemplate, threeVariablesTemplate, SortType.Variables, ["variable3"], 'resourceGroup()'); }); suite("Insert one variable in totally empty template", async () => { - await testInsertItem(totallyEmptyTemplate, oneVariableTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Variables, editor), ["variable1"], 'resourceGroup()'); + await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], '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, SortType.Parameters, ["parameter1", "String", "default", "description"]); + }); + suite("Insert one more parameter", async () => { + await doTestInsertItem(oneParameterTemplate, twoParametersTemplate, SortType.Parameters, ["parameter2", "String", "", ""]); + }); + suite("Insert even one more parameter", async () => { + await doTestInsertItem(twoParametersTemplate, threeParametersTemplate, SortType.Parameters, ["parameter3", "Secure string", "", "description3"]); + }); + suite("Insert one output in totally empty template", async () => { + await doTestInsertItem(totallyEmptyTemplate, oneParameterTemplate, SortType.Parameters, ["parameter1", "String", "default", "description"]); + }); + }); suite("Outputs", async () => { const emptyTemplate = `{ @@ -147,22 +214,22 @@ suite("InsertItem", async (): Promise => { "value": "[resourceGroup()]" }, "output3": { - "type": "string", + "type": "securestring", "value": "[resourceGroup()]" } } }`; suite("Insert one output", async () => { - await testInsertItem(emptyTemplate, oneOutputTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output1"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneOutputTemplate, SortType.Outputs, ["output1", "String"], 'resourceGroup()'); }); suite("Insert one more variable", async () => { - await testInsertItem(oneOutputTemplate, twoOutputsTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output2"], 'resourceGroup()'); + await doTestInsertItem(oneOutputTemplate, twoOutputsTemplate, SortType.Outputs, ["output2", "String"], 'resourceGroup()'); }); suite("Insert even one more variable", async () => { - await testInsertItem(twoOutputsTemplate, threeOutputsTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output3"], 'resourceGroup()'); + await doTestInsertItem(twoOutputsTemplate, threeOutputsTemplate, SortType.Outputs, ["output3", "Secure string"], 'resourceGroup()'); }); suite("Insert one output in totally empty template", async () => { - await testInsertItem(totallyEmptyTemplate, oneOutputTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Outputs, editor), ["output1"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneOutputTemplate, SortType.Outputs, ["output1", "String"], 'resourceGroup()'); }); }); }); @@ -174,11 +241,13 @@ class MockUserInput implements IAzureUserInput { } public async showQuickPick(items: T[] | Thenable, options: import("vscode-azureextensionui").IAzureQuickPickOptions): Promise { let result = await items; - return result[0]; + 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.pop()!; + return this.showInputBoxTexts.shift()!; } public async showWarningMessage(message: string, options: import("vscode-azureextensionui").IAzureMessageOptions, ...items: T[]): Promise { From 9e319e568909c49e545fe54c2709d155b20f4963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 10 Apr 2020 18:02:33 +0200 Subject: [PATCH 37/61] Added unit tests for Insert Function --- src/insertItem.ts | 4 +- test/functional/insertItem.test.ts | 135 +++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 8159bf4cf..0ce3898a9 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -378,9 +378,9 @@ export class InsertItem { 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.indexOf(insertCursorText) >= 0) { + if (text.lastIndexOf(insertCursorText) >= 0) { let insertedText = textEditor.document.getText(new vscode.Range(pos, textEditor.document.positionAt(index + text.length))); - let cursorPos = insertedText.indexOf(insertCursorText); + let cursorPos = insertedText.lastIndexOf(insertCursorText); let pos2 = textEditor.document.positionAt(index + cursorPos + insertCursorText.length / 2); textEditor.selection = new vscode.Selection(pos2, pos2); } diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 75a37a4c6..5fe4f741c 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -57,6 +57,9 @@ suite("InsertItem", async (): Promise => { 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); @@ -115,6 +118,138 @@ suite("InsertItem", async (): Promise => { }); }); + 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, SortType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); + }); + suite("Insert one more function", async () => { + await doTestInsertItem(oneFunctionTemplate, twoFunctionsTemplate, SortType.Functions, ["function2", "String", ""], "resourceGroup()"); + }); + suite("Insert one function in totally empty template", async () => { + await doTestInsertItem(totallyEmptyTemplate, oneFunctionTemplate, SortType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); + }); + suite("Insert function in namespace", async () => { + await doTestInsertItem(namespaceTemplate, oneFunctionTemplate, SortType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); + }); + suite("Insert function in members", async () => { + await doTestInsertItem(membersTemplate, oneFunctionTemplate, SortType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); + }); + suite("Insert even one more function", async () => { + await doTestInsertItem(twoFunctionsTemplate, threeFunctionsTemplate, SortType.Functions, ["function3", "Secure string", "parameter1", "String", "parameter2", "Bool", ""], "resourceGroup()"); + }); + }); suite("Parameters", async () => { const emptyTemplate = `{ From 274e59626987ce82c1977421d0007fcf00443ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 10 Apr 2020 19:52:48 +0200 Subject: [PATCH 38/61] Added one unit test for InsertResource --- src/insertItem.ts | 33 ++++++++++++++++++------------ test/functional/insertItem.test.ts | 29 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 0ce3898a9..2b8e7bdb7 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -296,21 +296,28 @@ export class InsertItem { private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); - if (!resources) { - return; - } + let pos: vscode.Position; const resource = await this.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); - let index = resources.span.endIndex; - let text = "\r\n\t\t"; - if (resources.elements.length > 0) { - let lastIndex = resources.elements.length - 1; - index = resources.elements[lastIndex].span.afterEndIndex; - text = `,${text}`; + if (!resources) { + if (!template.topLevelValue) { + return; + } + let subPart: any = []; + await this.insertInObject2(template.topLevelValue, textEditor, subPart, "resources", 1); + pos = textEditor.selection.active; + } else { + let index = resources.span.endIndex; + let text = "\r\n\t\t"; + if (resources.elements.length > 0) { + let lastIndex = resources.elements.length - 1; + index = resources.elements[lastIndex].span.afterEndIndex; + text = `,${text}`; + } + await this.insertText(textEditor, index, text, false); + pos = textEditor.document.positionAt(index + text.length); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; } - await this.insertText(textEditor, index, text, false); - let pos = textEditor.document.positionAt(index + text.length); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 5fe4f741c..8e740671b 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -117,6 +117,35 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], 'resourceGroup()'); }); }); + suite("Resources", async () => { + const emptyTemplate = + `{ + "resources": [] +}`; + const oneResourceTemplate = `{ + "resources": [ + { + "name": "storageaccount1", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-06-01", + "tags": { + "displayName": "storageaccount1" + }, + "location": "[resourceGroup().location]", + "kind": "StorageV2", + "sku": { + "name": "Premium_LRS", + "tier": "Premium" + } + } ] +}`; + + suite("Insert one resource (Storage Account)", async () => { + test("Spaces CRLF", async () => { + await testInsertItemWithSettings(emptyTemplate, oneResourceTemplate, true, 4, true, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Resources, editor), ["Storage Account"]); + }); + }); + }); suite("Functions", async () => { const emptyTemplate = From 38b4370d4379dcaa6a3fa442ccae5e7ad6e461fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 11 Apr 2020 01:39:58 +0200 Subject: [PATCH 39/61] Added test that verifies all snippets used by Insert Resource --- src/insertItem.ts | 144 ++++++++++++++--------------- test/functional/insertItem.test.ts | 21 ++++- 2 files changed, 92 insertions(+), 73 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 2b8e7bdb7..3e6138095 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -38,77 +38,6 @@ export function getItemType(): QuickPickItem[] { return items; } -export function getResourceSnippets(): vscode.QuickPickItem[] { - let items: vscode.QuickPickItem[] = []; - items.push(getQuickPickItem("Nested Deployment")); - items.push(getQuickPickItem("App Service Plan (Server Farm)")); - items.push(getQuickPickItem("Application Insights for Web Apps")); - items.push(getQuickPickItem("Application Security Group")); - items.push(getQuickPickItem("Automation Account")); - items.push(getQuickPickItem("Automation Certificate")); - items.push(getQuickPickItem("Automation Credential")); - items.push(getQuickPickItem("Automation Job Schedule")); - items.push(getQuickPickItem("Automation Runbook")); - items.push(getQuickPickItem("Automation Schedule")); - items.push(getQuickPickItem("Automation Variable")); - items.push(getQuickPickItem("Automation Module")); - items.push(getQuickPickItem("Availability Set")); - items.push(getQuickPickItem("Azure Firewall")); - items.push(getQuickPickItem("Container Group")); - items.push(getQuickPickItem("Container Registry")); - items.push(getQuickPickItem("Cosmos DB Database Account")); - items.push(getQuickPickItem("Cosmos DB SQL Database")); - items.push(getQuickPickItem("Cosmos DB Mongo Database")); - items.push(getQuickPickItem("Cosmos DB Gremlin Database")); - items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); - items.push(getQuickPickItem("Cosmos DB Cassandra Table")); - items.push(getQuickPickItem("Cosmos DB SQL Container")); - items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); - items.push(getQuickPickItem("Cosmos DB Table Storage Table")); - items.push(getQuickPickItem("Data Lake Store Account")); - items.push(getQuickPickItem("DNS Record")); - items.push(getQuickPickItem("DNS Zone")); - items.push(getQuickPickItem("Function")); - items.push(getQuickPickItem("KeyVault")); - items.push(getQuickPickItem("KeyVault Secret")); - items.push(getQuickPickItem("Kubernetes Service Cluster")); - items.push(getQuickPickItem("Linux VM Custom Script")); - items.push(getQuickPickItem("Load Balancer External")); - items.push(getQuickPickItem("Load Balancer Internal")); - items.push(getQuickPickItem("Log Analytics Solution")); - items.push(getQuickPickItem("Log Analytics Workspace")); - items.push(getQuickPickItem("Logic App")); - items.push(getQuickPickItem("Logic App Connector")); - items.push(getQuickPickItem("Managed Identity (User Assigned)")); - items.push(getQuickPickItem("Media Services")); - items.push(getQuickPickItem("MySQL Database")); - items.push(getQuickPickItem("Network Interface")); - items.push(getQuickPickItem("Network Security Group")); - items.push(getQuickPickItem("Network Security Group Rule")); - items.push(getQuickPickItem("Public IP Address")); - items.push(getQuickPickItem("Public IP Prefix")); - items.push(getQuickPickItem("Recovery Service Vault")); - items.push(getQuickPickItem("Redis Cache")); - items.push(getQuickPickItem("Route Table")); - items.push(getQuickPickItem("Route Table Route")); - items.push(getQuickPickItem("SQL Database")); - items.push(getQuickPickItem("SQL Database Import")); - items.push(getQuickPickItem("SQL Server")); - items.push(getQuickPickItem("Storage Account")); - items.push(getQuickPickItem("Traffic Manager Profile")); - items.push(getQuickPickItem("Ubuntu Virtual Machine")); - items.push(getQuickPickItem("Virtual Network")); - items.push(getQuickPickItem("VPN Local Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Connection")); - items.push(getQuickPickItem("Web App")); - items.push(getQuickPickItem("Web Deploy for Web App")); - items.push(getQuickPickItem("Windows Virtual Machine")); - items.push(getQuickPickItem("Windows VM Custom Script")); - items.push(getQuickPickItem("Windows VM Diagnostics Extension")); - items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); - return items; -} export function getQuickPickItem(label: string): vscode.QuickPickItem { return { label: label }; } @@ -128,6 +57,77 @@ export class InsertItem { constructor(ui: IAzureUserInput) { this.ui = ui; } + public getResourceSnippets(): vscode.QuickPickItem[] { + let items: vscode.QuickPickItem[] = []; + items.push(getQuickPickItem("Nested Deployment")); + items.push(getQuickPickItem("App Service Plan (Server Farm)")); + items.push(getQuickPickItem("Application Insights for Web Apps")); + items.push(getQuickPickItem("Application Security Group")); + items.push(getQuickPickItem("Automation Account")); + items.push(getQuickPickItem("Automation Certificate")); + items.push(getQuickPickItem("Automation Credential")); + items.push(getQuickPickItem("Automation Job Schedule")); + items.push(getQuickPickItem("Automation Runbook")); + items.push(getQuickPickItem("Automation Schedule")); + items.push(getQuickPickItem("Automation Variable")); + items.push(getQuickPickItem("Automation Module")); + items.push(getQuickPickItem("Availability Set")); + items.push(getQuickPickItem("Azure Firewall")); + items.push(getQuickPickItem("Container Group")); + items.push(getQuickPickItem("Container Registry")); + items.push(getQuickPickItem("Cosmos DB Database Account")); + items.push(getQuickPickItem("Cosmos DB SQL Database")); + items.push(getQuickPickItem("Cosmos DB Mongo Database")); + items.push(getQuickPickItem("Cosmos DB Gremlin Database")); + items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); + items.push(getQuickPickItem("Cosmos DB Cassandra Table")); + items.push(getQuickPickItem("Cosmos DB SQL Container")); + items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); + items.push(getQuickPickItem("Cosmos DB Table Storage Table")); + items.push(getQuickPickItem("Data Lake Store Account")); + items.push(getQuickPickItem("DNS Record")); + items.push(getQuickPickItem("DNS Zone")); + items.push(getQuickPickItem("Function")); + items.push(getQuickPickItem("KeyVault")); + items.push(getQuickPickItem("KeyVault Secret")); + items.push(getQuickPickItem("Kubernetes Service Cluster")); + items.push(getQuickPickItem("Linux VM Custom Script")); + items.push(getQuickPickItem("Load Balancer External")); + items.push(getQuickPickItem("Load Balancer Internal")); + items.push(getQuickPickItem("Log Analytics Solution")); + items.push(getQuickPickItem("Log Analytics Workspace")); + items.push(getQuickPickItem("Logic App")); + items.push(getQuickPickItem("Logic App Connector")); + items.push(getQuickPickItem("Managed Identity (User Assigned)")); + items.push(getQuickPickItem("Media Services")); + items.push(getQuickPickItem("MySQL Database")); + items.push(getQuickPickItem("Network Interface")); + items.push(getQuickPickItem("Network Security Group")); + items.push(getQuickPickItem("Network Security Group Rule")); + items.push(getQuickPickItem("Public IP Address")); + items.push(getQuickPickItem("Public IP Prefix")); + items.push(getQuickPickItem("Recovery Service Vault")); + items.push(getQuickPickItem("Redis Cache")); + items.push(getQuickPickItem("Route Table")); + items.push(getQuickPickItem("Route Table Route")); + items.push(getQuickPickItem("SQL Database")); + items.push(getQuickPickItem("SQL Database Import")); + items.push(getQuickPickItem("SQL Server")); + items.push(getQuickPickItem("Storage Account")); + items.push(getQuickPickItem("Traffic Manager Profile")); + items.push(getQuickPickItem("Ubuntu Virtual Machine")); + items.push(getQuickPickItem("Virtual Network")); + items.push(getQuickPickItem("VPN Local Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Connection")); + items.push(getQuickPickItem("Web App")); + items.push(getQuickPickItem("Web Deploy for Web App")); + items.push(getQuickPickItem("Windows Virtual Machine")); + items.push(getQuickPickItem("Windows VM Custom Script")); + items.push(getQuickPickItem("Windows VM Diagnostics Extension")); + items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); + return items; + } public async insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { if (!template) { @@ -297,7 +297,7 @@ export class InsertItem { private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); let pos: vscode.Position; - const resource = await this.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); + const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); if (!resources) { if (!template.topLevelValue) { return; diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 8e740671b..f4bf66c9d 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -11,7 +11,7 @@ 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 { commands, window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; @@ -73,6 +73,16 @@ suite("InsertItem", async (): Promise => { assertTemplate(docTextAfterInsertion, expected, textEditor); } + async function testResourceSnippet(resourceSnippet: string): Promise { + const tempPath = getTempFilePath(`insertItem`, '.azrm'); + fse.writeFileSync(tempPath, ''); + let document = await workspace.openTextDocument(tempPath); + await window.showTextDocument(document); + let timeout = setTimeout(() => assert.fail(`Invalid resource snippet: ${resourceSnippet}`), 1000); + await commands.executeCommand('editor.action.insertSnippet', { name: resourceSnippet }); + clearTimeout(timeout); + } + const totallyEmptyTemplate = `{}`; @@ -145,6 +155,15 @@ suite("InsertItem", async (): Promise => { await testInsertItemWithSettings(emptyTemplate, oneResourceTemplate, true, 4, true, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Resources, editor), ["Storage Account"]); }); }); + + suite("Resource snippets", async () => { + test("Verify all snippets used by Insert Resource", async () => { + let insertItem = new InsertItem(new MockUserInput([])); + for (const snippet of insertItem.getResourceSnippets()) { + await testResourceSnippet(snippet.label); + } + }); + }); }); suite("Functions", async () => { From a15465bf67f2ae46ec645c16c5c13bdca696b403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 17:30:10 +0200 Subject: [PATCH 40/61] Improved tests for Insert Resource --- src/insertItem.ts | 32 ++++++----- test/functional/insertItem.test.ts | 87 +++++++++++++++++++----------- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 3e6138095..984949a9d 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -213,7 +213,7 @@ export class InsertItem { } // tslint:disable-next-line:no-any - private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { + private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { let firstItem = templatePart.properties.length === 0; let startText = firstItem ? '' : ','; let index = firstItem ? templatePart.span.endIndex : templatePart.properties[templatePart.properties.length - 1].span.afterEndIndex; @@ -222,7 +222,7 @@ export class InsertItem { let text = typeof (data) === 'object' ? JSON.stringify(data, null, '\t') : `"${data}"`; let indentedText = this.indent(`\r\n"${name}": ${text}`, indentLevel); - await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); + return await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); } private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { @@ -297,27 +297,32 @@ export class InsertItem { private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); let pos: vscode.Position; + let index: number; + let text = "\r\n\t\t\r\n\t"; const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); if (!resources) { if (!template.topLevelValue) { return; } let subPart: any = []; - await this.insertInObject2(template.topLevelValue, textEditor, subPart, "resources", 1); + index = await this.insertInObject2(template.topLevelValue, textEditor, subPart, "resources", 1); pos = textEditor.selection.active; } else { - let index = resources.span.endIndex; - let text = "\r\n\t\t"; + index = resources.span.endIndex; if (resources.elements.length > 0) { let lastIndex = resources.elements.length - 1; index = resources.elements[lastIndex].span.afterEndIndex; - text = `,${text}`; + text = `,\r\n\t\t`; } - await this.insertText(textEditor, index, text, false); - pos = textEditor.document.positionAt(index + text.length); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; } + await this.insertText(textEditor, index, text, false); + let range = new vscode.Range(textEditor.document.positionAt(index), textEditor.document.positionAt(index + this.formatText(text, textEditor).length)); + let insertedText = textEditor.document.getText(range); + let lookFor = this.formatText('\t\t', textEditor); + let cursorPos = insertedText.indexOf(lookFor); + pos = textEditor.document.positionAt(index + cursorPos + lookFor.length); + let newSelection = new vscode.Selection(pos, pos); + textEditor.selection = newSelection; await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); } @@ -380,7 +385,7 @@ export class InsertItem { return members; } - private async insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { + private async insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { text = this.formatText(text, textEditor); let pos = textEditor.document.positionAt(index); await textEditor.edit(builder => builder.insert(pos, text)); @@ -388,9 +393,12 @@ export class InsertItem { 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 pos2 = textEditor.document.positionAt(index + cursorPos + insertCursorText.length / 2); + 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 { diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index f4bf66c9d..ace4f88cd 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -17,11 +17,19 @@ import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { - function assertTemplate(actual: String, expected: String, textEditor: vscode.TextEditor): void { + 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'); @@ -29,28 +37,28 @@ suite("InsertItem", async (): Promise => { assert.equal(actual, expected); } - async function testInsertItem(template: string, expected: String, action: (insertItem: InsertItem, deploymentTemplate: DeploymentTemplate, textEditor: vscode.TextEditor) => Promise, showInputBox: string[], textToInsert: string = ''): Promise { + 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); + 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); + 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); + 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); + 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); + 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); + 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 = ''): Promise { + 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'); } @@ -70,7 +78,7 @@ suite("InsertItem", async (): Promise => { await action(insertItem, deploymentTemplate, textEditor); await textEditor.edit(builder => builder.insert(textEditor.selection.active, textToInsert)); const docTextAfterInsertion = document.getText(); - assertTemplate(docTextAfterInsertion, expected, textEditor); + assertTemplate(docTextAfterInsertion, expected, textEditor, ignoreWhiteSpace); } async function testResourceSnippet(resourceSnippet: string): Promise { @@ -86,8 +94,8 @@ suite("InsertItem", async (): Promise => { const totallyEmptyTemplate = `{}`; - async function doTestInsertItem(startTemplate: string, expectedTemplate: string, type: SortType, showInputBox: string[] = [], textToInsert: string = ''): Promise { - await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, type, editor), showInputBox, textToInsert); + async function doTestInsertItem(startTemplate: string, expectedTemplate: string, type: SortType, showInputBox: string[] = [], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise { + await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, type, editor), showInputBox, textToInsert, ignoreWhiteSpace); } suite("Variables", async () => { @@ -134,26 +142,45 @@ suite("InsertItem", async (): Promise => { }`; const oneResourceTemplate = `{ "resources": [ - { - "name": "storageaccount1", - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2019-06-01", - "tags": { - "displayName": "storageaccount1" - }, - "location": "[resourceGroup().location]", - "kind": "StorageV2", - "sku": { - "name": "Premium_LRS", - "tier": "Premium" - } - } ] + { + "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 (Storage Account)", async () => { - test("Spaces CRLF", async () => { - await testInsertItemWithSettings(emptyTemplate, oneResourceTemplate, true, 4, true, async (insertItem, template, editor) => await insertItem.insertItem(template, SortType.Resources, editor), ["Storage Account"]); - }); + suite("Insert one resource (KeyVault Secret) into totally empty template", async () => { + await doTestInsertItem(totallyEmptyTemplate, oneResourceTemplate, SortType.Resources, ["KeyVault Secret"], '', true); + }); + suite("Insert one resource (KeyVault Secret)", async () => { + await doTestInsertItem(emptyTemplate, oneResourceTemplate, SortType.Resources, ["KeyVault Secret"], '', true); + }); + suite("Insert one more resource (Application Security Group)", async () => { + await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, SortType.Resources, ["Application Security Group"], '', true); }); suite("Resource snippets", async () => { From a01ceecbf0af5e835ede11bcafdf6641d296e438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 18:04:57 +0200 Subject: [PATCH 41/61] Added error and information messages --- src/insertItem.ts | 78 +++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 984949a9d..7fe20d26f 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -136,25 +136,34 @@ export class InsertItem { ext.outputChannel.appendLine("Insert item"); switch (sortType) { case SortType.Functions: - await this.insertFunction(template, textEditor); + if (await this.insertFunction(template, textEditor)) { + vscode.window.showInformationMessage("Please type the output of the function."); + } break; case SortType.Outputs: - await this.insertOutput(template, textEditor); + if (await this.insertOutput(template, textEditor)) { + vscode.window.showInformationMessage("Please type the the value of the output."); + } break; case SortType.Parameters: - await this.insertParameter(template, textEditor); + if (await this.insertParameter(template, textEditor)) { + vscode.window.showInformationMessage("Done inserting parameter."); + } break; case SortType.Resources: - await this.insertResource(template, textEditor); + if (await this.insertResource(template, textEditor)) { + vscode.window.showInformationMessage("Press TAB to move between the tab stops."); + } break; case SortType.Variables: - await this.insertVariable(template, textEditor); + if (await this.insertVariable(template, textEditor)) { + vscode.window.showInformationMessage("Please type the the value of the variable."); + } break; default: vscode.window.showWarningMessage("Unknown insert item type!"); return; } - vscode.window.showInformationMessage("Done inserting item!"); } private getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { @@ -175,7 +184,7 @@ export class InsertItem { return rootValue.getPropertyValue(templatePart); } - private async insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): 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 = { @@ -191,25 +200,27 @@ export class InsertItem { description: description }; } - await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name); + return await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name); } // tslint:disable-next-line:no-any - private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { + private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { let templatePart = this.getTemplateObjectPart(template, part); if (!templatePart) { let topLevel = template.topLevelValue; if (!topLevel) { - return; + vscode.window.showErrorMessage('Invalid ARM template!'); + return false; } // tslint:disable-next-line:no-any let subPart: any = {}; // tslint:disable-next-line:no-unsafe-any subPart[name] = data; await this.insertInObject2(topLevel, textEditor, subPart, part, 1); - return; + } else { + await this.insertInObject2(templatePart, textEditor, data, name); } - await this.insertInObject2(templatePart, textEditor, data, name); + return true; } // tslint:disable-next-line:no-any @@ -221,35 +232,37 @@ export class InsertItem { let endText = firstItem ? `\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): Promise { + private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let name = await this.ui.showInputBox({ prompt: "Name of variable?" }); - await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); + return await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); } - private async insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): 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); + return await this.insertInObject(template, textEditor, templateKeys.outputs, output, name); } - private async insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { + private async insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { if (!topLevel) { - return; + vscode.window.showErrorMessage('Invalid ARM template!'); + return false; } let functions = [await this.getFunctionNamespace()]; await this.insertInObject2(topLevel, textEditor, functions, "functions", 1); + return true; } - private async insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { + private async insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { let namespace = await this.getFunctionNamespace(); await this.insertInArray(functions, textEditor, namespace); + return true; } private async insertFunctionMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { @@ -268,33 +281,35 @@ export class InsertItem { await this.insertInObject2(members, textEditor, functionDef, functionName, 4); } - private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let functions = this.getTemplateArrayPart(template, templateKeys.functions); if (!functions) { - await this.insertFunctionTopLevel(template.topLevelValue, textEditor); - return; + // tslint:disable-next-line:no-unsafe-any + return await this.insertFunctionTopLevel(template.topLevelValue, textEditor); } if (functions.length === 0) { - await this.insertFunctionNamespace(functions, textEditor); - return; + return await this.insertFunctionNamespace(functions, textEditor); } let namespace = Json.asObjectValue(functions.elements[0]); if (!namespace) { - return; + vscode.window.showErrorMessage('The first namespace in functions is not an object!'); + return false; } let members = namespace.getPropertyValue("members"); if (!members) { await this.insertFunctionMembers(namespace, textEditor); - return; + return true; } let membersObject = Json.asObjectValue(members); if (!membersObject) { - return; + vscode.window.showErrorMessage('The first namespace in functions does not have members as an object!'); + return false; } await this.insertFunctionFunction(membersObject, textEditor); + return true; } - private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); let pos: vscode.Position; let index: number; @@ -302,8 +317,10 @@ export class InsertItem { const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); if (!resources) { if (!template.topLevelValue) { - return; + vscode.window.showErrorMessage("Invalid ARM template!"); + return false; } + // tslint:disable-next-line:no-any let subPart: any = []; index = await this.insertInObject2(template.topLevelValue, textEditor, subPart, "resources", 1); pos = textEditor.selection.active; @@ -325,6 +342,7 @@ export class InsertItem { textEditor.selection = newSelection; await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); + return true; } private async getFunction(): Promise { From 4437a36562ee89ed92ac4391c86e8aead4ef28ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 18:31:32 +0200 Subject: [PATCH 42/61] Cleanup of code --- src/insertItem.ts | 46 +++++++++++++++--------------- test/functional/insertItem.test.ts | 7 +++-- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 7fe20d26f..d89ad6589 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -190,11 +190,11 @@ export class InsertItem { let parameter: Parameter = { type: parameterType.value }; - let defaultValue = await this.ui.showInputBox({ prompt: "Default value? Leave empty for no default 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", }); + let description = await this.ui.showInputBox({ prompt: "Description? Leave empty for no description.", }); if (description !== '') { parameter.metadata = { description: description @@ -216,20 +216,20 @@ export class InsertItem { let subPart: any = {}; // tslint:disable-next-line:no-unsafe-any subPart[name] = data; - await this.insertInObject2(topLevel, textEditor, subPart, part, 1); + await this.insertInObjectInternal(topLevel, textEditor, subPart, part, 1); } else { - await this.insertInObject2(templatePart, textEditor, data, name); + await this.insertInObjectInternal(templatePart, textEditor, data, name); } return true; } // tslint:disable-next-line:no-any - private async insertInObject2(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { - let firstItem = templatePart.properties.length === 0; - let startText = firstItem ? '' : ','; - let index = firstItem ? templatePart.span.endIndex : templatePart.properties[templatePart.properties.length - 1].span.afterEndIndex; + private async insertInObjectInternal(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 = firstItem ? `\r\n${tabs}` : ``; + 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}`); @@ -249,46 +249,46 @@ export class InsertItem { }; return await this.insertInObject(template, textEditor, templateKeys.outputs, output, name); } - private async insertFunctionTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { + private async insertFunctionAsTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { if (!topLevel) { vscode.window.showErrorMessage('Invalid ARM template!'); return false; } let functions = [await this.getFunctionNamespace()]; - await this.insertInObject2(topLevel, textEditor, functions, "functions", 1); + await this.insertInObjectInternal(topLevel, textEditor, functions, "functions", 1); return true; } - private async insertFunctionNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { + private async insertFunctionAsNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { let namespace = await this.getFunctionNamespace(); await this.insertInArray(functions, textEditor, namespace); return true; } - private async insertFunctionMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { + 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.insertInObject2(namespace, textEditor, members, 'members', 3); + await this.insertInObjectInternal(namespace, textEditor, members, 'members', 3); } - private async insertFunctionFunction(members: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { + 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.insertInObject2(members, textEditor, functionDef, functionName, 4); + await this.insertInObjectInternal(members, textEditor, functionDef, functionName, 4); } private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { let functions = this.getTemplateArrayPart(template, templateKeys.functions); if (!functions) { // tslint:disable-next-line:no-unsafe-any - return await this.insertFunctionTopLevel(template.topLevelValue, textEditor); + return await this.insertFunctionAsTopLevel(template.topLevelValue, textEditor); } if (functions.length === 0) { - return await this.insertFunctionNamespace(functions, textEditor); + return await this.insertFunctionAsNamespace(functions, textEditor); } let namespace = Json.asObjectValue(functions.elements[0]); if (!namespace) { @@ -297,7 +297,7 @@ export class InsertItem { } let members = namespace.getPropertyValue("members"); if (!members) { - await this.insertFunctionMembers(namespace, textEditor); + await this.insertFunctionAsMembers(namespace, textEditor); return true; } let membersObject = Json.asObjectValue(members); @@ -305,7 +305,7 @@ export class InsertItem { vscode.window.showErrorMessage('The first namespace in functions does not have members as an object!'); return false; } - await this.insertFunctionFunction(membersObject, textEditor); + await this.insertFunctionAsFunction(membersObject, textEditor); return true; } @@ -314,7 +314,6 @@ export class InsertItem { let pos: vscode.Position; let index: number; let text = "\r\n\t\t\r\n\t"; - const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); if (!resources) { if (!template.topLevelValue) { vscode.window.showErrorMessage("Invalid ARM template!"); @@ -322,7 +321,7 @@ export class InsertItem { } // tslint:disable-next-line:no-any let subPart: any = []; - index = await this.insertInObject2(template.topLevelValue, textEditor, subPart, "resources", 1); + index = await this.insertInObjectInternal(template.topLevelValue, textEditor, subPart, "resources", 1); pos = textEditor.selection.active; } else { index = resources.span.endIndex; @@ -332,6 +331,7 @@ export class InsertItem { text = `,\r\n\t\t`; } } + const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); await this.insertText(textEditor, index, text, false); let range = new vscode.Range(textEditor.document.positionAt(index), textEditor.document.positionAt(index + this.formatText(text, textEditor).length)); let insertedText = textEditor.document.getText(range); @@ -352,7 +352,7 @@ export class InsertItem { parameters: parameters, output: { type: outputType.value, - value: insertCursorText.replace(/"/g, '') + value: insertCursorText } }; return functionDef; diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index ace4f88cd..195f110ee 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -135,6 +135,7 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], 'resourceGroup()'); }); }); + suite("Resources", async () => { const emptyTemplate = `{ @@ -325,6 +326,7 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(twoFunctionsTemplate, threeFunctionsTemplate, SortType.Functions, ["function3", "Secure string", "parameter1", "String", "parameter2", "Bool", ""], "resourceGroup()"); }); }); + suite("Parameters", async () => { const emptyTemplate = `{ @@ -388,6 +390,7 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(totallyEmptyTemplate, oneParameterTemplate, SortType.Parameters, ["parameter1", "String", "default", "description"]); }); }); + suite("Outputs", async () => { const emptyTemplate = `{ @@ -432,10 +435,10 @@ suite("InsertItem", async (): Promise => { suite("Insert one output", async () => { await doTestInsertItem(emptyTemplate, oneOutputTemplate, SortType.Outputs, ["output1", "String"], 'resourceGroup()'); }); - suite("Insert one more variable", async () => { + suite("Insert one more output", async () => { await doTestInsertItem(oneOutputTemplate, twoOutputsTemplate, SortType.Outputs, ["output2", "String"], 'resourceGroup()'); }); - suite("Insert even one more variable", async () => { + suite("Insert even one more output", async () => { await doTestInsertItem(twoOutputsTemplate, threeOutputsTemplate, SortType.Outputs, ["output3", "Secure string"], 'resourceGroup()'); }); suite("Insert one output in totally empty template", async () => { From 3b9eab9517c75c627b4c6a4736b72915c6adcb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 19:45:04 +0200 Subject: [PATCH 43/61] Merge from master --- assets/armsnippets.jsonc | 4 +- extension.bundle.ts | 16 +- gulpfile.ts | 6 +- package-lock.json | 8 +- package.json | 84 +- src/AzureRMTools.ts | 958 +++++++++++++----- src/Completion.ts | 96 +- src/CompletionsSpy.ts | 41 + src/Configuration.ts | 32 + src/DeploymentDocument.ts | 172 ++++ src/DeploymentTemplate.ts | 306 ++---- src/INamedDefinition.ts | 3 + src/JSON.ts | 230 ++++- src/Language.ts | 148 ++- src/PositionContext.ts | 736 ++------------ src/ReferenceList.ts | 33 +- src/TLE.ts | 28 +- src/TemplatePositionContext.ts | 696 +++++++++++++ src/Tokenizer.ts | 3 + src/Treeview.ts | 23 +- src/UserFunctionNamespaceDefinition.ts | 2 +- src/VariableDefinition.ts | 2 +- src/constants.ts | 2 +- src/debugMarkStrings.ts | 28 +- src/extensionVariables.ts | 31 +- src/languageclient/startArmLanguageServer.ts | 8 +- ...eterFile.ts => parameterFileGeneration.ts} | 50 +- src/parameterFiles/DeploymentFileMapping.ts | 142 +++ src/parameterFiles/DeploymentParameters.ts | 298 ++++++ .../ParameterValueDefinition.ts | 56 + .../ParametersPositionContext.ts | 236 +++++ src/{ => parameterFiles}/parameterFiles.ts | 167 ++- src/parameterFiles/setParameterFileContext.ts | 49 + src/supported.ts | 61 +- src/util/InitializeBeforeUse.ts | 25 + src/util/normalizePath.ts | 18 + src/util/throwOnCancel.ts | 30 + src/util/toVsCodeCompletionItem.ts | 85 ++ src/util/vscodePosition.ts | 10 +- src/visitors/FindReferencesVisitor.ts | 30 +- test/Completion.test.ts | 6 +- test/DeploymentFileMapping.test.ts | 193 ++++ test/DeploymentParameters.test.ts | 85 ++ test/DeploymentTemplate.test.ts | 295 +++--- test/JSON.test.ts | 283 ++++++ test/Language.test.ts | 175 +++- ...est.ts => ParameterFileGeneration.test.ts} | 50 +- test/ParametersPositionContext.test.ts | 171 ++++ test/Reference.test.ts | 43 +- test/TLE.test.ts | 76 +- ...est.ts => TemplatePositionContext.test.ts} | 327 +++--- test/TemplateTests.test.ts | 5 +- test/TestData.ts | 96 +- test/UserFunctions.test.ts | 126 +-- test/VariableIteration.test.ts | 23 +- .../inputs/expr-vs-string.is-expression.jsonc | 2 +- test/formatDocument.test.ts | 6 +- test/functional/insertItem.test.ts | 2 +- .../paramFileCompletions.functional.test.ts | 411 ++++++++ .../paramFiles.addMissingParameters.test.ts | 394 +++++++ test/functional/snippets.test.ts | 4 +- test/functional/sortTemplate.test.ts | 5 + test/functional/validation.regression.test.ts | 2 +- test/functional/validation.test.ts | 73 ++ test/global.test.ts | 4 +- test/parameterFileCompletions.test.ts | 298 ++++++ test/support/TempFile.ts | 82 ++ test/support/TestConfiguration.ts | 29 + test/support/armTest.ts | 42 - test/support/createCompletionsTest.ts | 7 +- test/support/diagnostics.ts | 45 +- test/support/getEventPromise.ts | 82 ++ test/support/parseTemplate.ts | 64 +- test/support/stringify.ts | 4 +- test/support/testGetReferences.ts | 4 +- test/support/testOnPlatform.ts | 52 + test/support/testStringAtEachIndex.ts | 58 ++ test/support/testWithLanguageServer.ts | 35 +- test/support/testWithPrep.ts | 57 ++ test/supported.test.ts | 23 +- test/templates/portal/new-vmscaleset1.json | 2 +- .../portal/new-vmscaleset1.parameters.json | 79 ++ test/templates/scopes/invalid-schema.json | 31 + ...GroupDeploymentTemplate.define-policy.json | 27 + .../resourceGroupDeployment2015-01-01.json | 31 + .../resourceGroupDeployment2019-04-01.json | 31 + .../scopes/subscriptionDeployment2.json | 60 ++ .../subscriptionDeploymentTemplate.json | 27 + ...iptionDeploymentWithNesteRGDeployment.json | 60 ++ .../tenantDeploymentTemplate.assign-role.json | 36 + tsconfig.json | 3 +- 91 files changed, 6998 insertions(+), 2081 deletions(-) create mode 100644 src/CompletionsSpy.ts create mode 100644 src/Configuration.ts create mode 100644 src/DeploymentDocument.ts create mode 100644 src/TemplatePositionContext.ts rename src/{editParameterFile.ts => parameterFileGeneration.ts} (69%) create mode 100644 src/parameterFiles/DeploymentFileMapping.ts create mode 100644 src/parameterFiles/DeploymentParameters.ts create mode 100644 src/parameterFiles/ParameterValueDefinition.ts create mode 100644 src/parameterFiles/ParametersPositionContext.ts rename src/{ => parameterFiles}/parameterFiles.ts (74%) create mode 100644 src/parameterFiles/setParameterFileContext.ts create mode 100644 src/util/InitializeBeforeUse.ts create mode 100644 src/util/normalizePath.ts create mode 100644 src/util/throwOnCancel.ts create mode 100644 src/util/toVsCodeCompletionItem.ts create mode 100644 test/DeploymentFileMapping.test.ts create mode 100644 test/DeploymentParameters.test.ts rename test/{editParameterFile.test.ts => ParameterFileGeneration.test.ts} (82%) create mode 100644 test/ParametersPositionContext.test.ts rename test/{PositionContext.test.ts => TemplatePositionContext.test.ts} (84%) create mode 100644 test/functional/paramFileCompletions.functional.test.ts create mode 100644 test/functional/paramFiles.addMissingParameters.test.ts create mode 100644 test/functional/validation.test.ts create mode 100644 test/parameterFileCompletions.test.ts create mode 100644 test/support/TempFile.ts create mode 100644 test/support/TestConfiguration.ts delete mode 100644 test/support/armTest.ts create mode 100644 test/support/getEventPromise.ts create mode 100644 test/support/testOnPlatform.ts create mode 100644 test/support/testStringAtEachIndex.ts create mode 100644 test/support/testWithPrep.ts create mode 100644 test/templates/portal/new-vmscaleset1.parameters.json create mode 100644 test/templates/scopes/invalid-schema.json create mode 100644 test/templates/scopes/managementGroupDeploymentTemplate.define-policy.json create mode 100644 test/templates/scopes/resourceGroupDeployment2015-01-01.json create mode 100644 test/templates/scopes/resourceGroupDeployment2019-04-01.json create mode 100644 test/templates/scopes/subscriptionDeployment2.json create mode 100644 test/templates/scopes/subscriptionDeploymentTemplate.json create mode 100644 test/templates/scopes/subscriptionDeploymentWithNesteRGDeployment.json create mode 100644 test/templates/scopes/tenantDeploymentTemplate.assign-role.json diff --git a/assets/armsnippets.jsonc b/assets/armsnippets.jsonc index c363d5a52..c96739714 100644 --- a/assets/armsnippets.jsonc +++ b/assets/armsnippets.jsonc @@ -78,7 +78,7 @@ "description": "ARM Template Variable" }, "Parameter": { - "prefix": "arm-param-value", + "prefix": "arm-param", "body": [ "\"${1:parameter1}\": {", " \"type\": \"${2|string,securestring,int,bool,object,secureobject,array|}\",", @@ -1439,7 +1439,7 @@ "{", " \"name\": \"${1:logicApp1}\",", " \"type\": \"Microsoft.Logic/workflows\",", - " \"apiVersion\": \"2017-07-01\",", + " \"apiVersion\": \"2019-05-01\",", " \"location\": \"[resourceGroup().location]\",", " \"properties\": {", " \"definition\": {", diff --git a/extension.bundle.ts b/extension.bundle.ts index 6a45fd512..0bec726c3 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -32,11 +32,12 @@ export { CachedPromise } from "./src/CachedPromise"; export { CachedValue } from "./src/CachedValue"; export { CaseInsensitiveMap } from "./src/CaseInsensitiveMap"; export * from "./src/Completion"; -export { basePath, configKeys, configPrefix, diagnosticsCompletePrefix, expressionsDiagnosticsSource, isWin32, languageId as armDeploymentLanguageId, languageId, languageServerStateSource, templateKeys } from "./src/constants"; -export { __debugMarkPositionInString, __debugMarkSubstring } from "./src/debugMarkStrings"; +export { CompletionsSpy, ICompletionsSpyResult } from "./src/CompletionsSpy"; +export { IConfiguration } from "./src/Configuration"; +export { armTemplateLanguageId, basePath, configKeys, configPrefix, diagnosticsCompletePrefix, expressionsDiagnosticsSource, isWin32, languageServerStateSource, templateKeys } from "./src/constants"; +export { __debugMarkPositionInString, __debugMarkRangeInString, __debugMarkRangeInString as __debugMarkSubstring } from "./src/debugMarkStrings"; export { DeploymentTemplate } from "./src/DeploymentTemplate"; export { Duration } from './src/Duration'; -export { createParameterFileContents, createParameterProperty } from './src/editParameterFile'; export { ExpressionType } from "./src/ExpressionType"; export { ext } from './src/extensionVariables'; export { Histogram } from "./src/Histogram"; @@ -49,12 +50,18 @@ export { IParameterDefinition } from "./src/IParameterDefinition"; export * from "./src/Language"; export { LanguageServerState } from "./src/languageclient/startArmLanguageServer"; export { ParameterDefinition } from "./src/ParameterDefinition"; -export { mayBeMatchingParameterFile } from "./src/parameterFiles"; +export { createParameterFileContents, createParameterFromTemplateParameter } from './src/parameterFileGeneration'; +export { DeploymentFileMapping } from "./src/parameterFiles/DeploymentFileMapping"; +export { DeploymentParameters } from "./src/parameterFiles/DeploymentParameters"; +export { mayBeMatchingParameterFile } from "./src/parameterFiles/parameterFiles"; +export { ParametersPositionContext } from "./src/parameterFiles/ParametersPositionContext"; +export { ParameterValueDefinition } from "./src/parameterFiles/ParameterValueDefinition"; export { IReferenceSite, PositionContext } from "./src/PositionContext"; export { ReferenceList } from "./src/ReferenceList"; export { containsArmSchema, getPreferredSchema, isArmSchema } from './src/schemas'; export { SortType } from "./src/sortTemplate"; export * from "./src/survey"; +export { TemplatePositionContext } from "./src/TemplatePositionContext"; export { ScopeContext, TemplateScope } from "./src/TemplateScope"; export { FunctionSignatureHelp } from "./src/TLE"; export { JsonOutlineProvider, shortenTreeLabel } from "./src/Treeview"; @@ -65,6 +72,7 @@ export { UserFunctionParameterDefinition } from "./src/UserFunctionParameterDefi export { mapJsonObjectValue } from "./src/util/mapJsonObjectValue"; export { indentMultilineString, unindentMultilineString as removeIndentation } from "./src/util/multilineStrings"; export * from "./src/util/nonNull"; +export { normalizePath } from "./src/util/normalizePath"; export * from './src/util/time'; export { getVSCodePositionFromPosition } from "./src/util/vscodePosition"; export { wrapError } from "./src/util/wrapError"; diff --git a/gulpfile.ts b/gulpfile.ts index ddd56c169..f76414e27 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -303,7 +303,7 @@ async function packageVsix(): Promise { // When webpacked, the tests cannot touch any code under src/, or it will end up getting loaded // twice (because it's also in the bundle), which causes problems with objects that are supposed to // be singletons. The test errors are somewhat mysterious, so verify that condition here during build. -async function verifyTestReferencesOnlyExtensionBundle(testFolder: string): Promise { +async function verifyTestsReferenceOnlyExtensionBundle(testFolder: string): Promise { const errors: string[] = []; for (let filePath of await recursiveReadDir(testFolder)) { @@ -320,7 +320,7 @@ async function verifyTestReferencesOnlyExtensionBundle(testFolder: string): Prom errors.push( os.EOL + `${path.relative(__dirname, file)}: error: Test code may not import from the src folder, it should import from '../extension.bundle'${os.EOL}` + - `Imported here: ${match}${os.EOL}` + ` Error is here: ===> ${match}${os.EOL}` ); console.error(match); } @@ -341,4 +341,4 @@ exports['watch-grammars'] = (): unknown => gulp.watch('grammars/**', buildGramma exports['get-language-server'] = getLanguageServer; exports.package = packageVsix; exports['error-vsce-package'] = (): never => { throw new Error(`Please do not run vsce package, instead use 'npm run package`); }; -exports['verify-test-uses-extension-bundle'] = (): Promise => verifyTestReferencesOnlyExtensionBundle(path.resolve("test")); +exports['verify-test-uses-extension-bundle'] = (): Promise => verifyTestsReferenceOnlyExtensionBundle(path.resolve("test")); diff --git a/package-lock.json b/package-lock.json index ecac629c9..d15b59075 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "azurerm-vscode-tools", - "version": "0.9.0", + "version": "0.9.1-alpha", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8249,9 +8249,9 @@ "dev": true }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index 56a281acf..eec9bef60 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "azurerm-vscode-tools", "displayName": "Azure Resource Manager (ARM) Tools", "description": "Language server, editing tools and snippets for Azure Resource Manager (ARM) template files.", - "version": "0.9.0", + "version": "0.9.1-alpha", "publisher": "msazurermtools", "config": { - "ARM_LANGUAGE_SERVER_NUGET_VERSION": "3.0.0-preview.20201.3" + "ARM_LANGUAGE_SERVER_NUGET_VERSION": "3.0.0-preview.20209.4" }, "categories": [ "Azure", @@ -52,13 +52,16 @@ "onCommand:azurerm-vscode-tools.sortVariables", "onCommand:azurerm-vscode-tools.selectParameterFile", "onCommand:azurerm-vscode-tools.openParameterFile", - "onCommand:azurerm-vscode-tools.resetGlobalState", + "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.insertResource", + "onCommand:azurerm-vscode-tools.resetGlobalState" ], "contributes": { "grammars": [ @@ -162,11 +165,18 @@ ], "commands": [ { + "$comment": "============= General commands =============", "category": "Azure Resource Manager Tools", "title": "Remove Local Dotnet Core Installation", "command": "azurerm-vscode-tools.uninstallDotnet" }, { + "category": "Azure Resource Manager Tools", + "title": "Reset Global State", + "command": "azurerm-vscode-tools.resetGlobalState" + }, + { + "$comment": "============= Template sorting =============", "category": "Azure Resource Manager Tools", "title": "Sort Template...", "command": "azurerm-vscode-tools.sortTemplate" @@ -197,6 +207,7 @@ "command": "azurerm-vscode-tools.sortVariables" }, { + "$comment": "============= Template file commands =============", "category": "Azure Resource Manager Tools", "title": "Select/Create Parameter File...", "command": "azurerm-vscode-tools.selectParameterFile", @@ -206,12 +217,31 @@ "category": "Azure Resource Manager Tools", "title": "Open Parameter File", "command": "azurerm-vscode-tools.openParameterFile", - "enablement": "editorLangId==arm-template" + "enablement": "azurerm-vscode-tools-hasParamFile", + "$enablement.comment": "Shows up when it's a template file, but only enabled if there is an associated param file" }, { + "$comment": "============= Parameter file commands =============", "category": "Azure Resource Manager Tools", - "title": "Reset Global State", - "command": "azurerm-vscode-tools.resetGlobalState" + "title": "Open Template File", + "command": "azurerm-vscode-tools.openTemplateFile", + "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" + }, + { + "$comment": "============= Parameter file commands =============", + "category": "Azure Resource Manager Tools", + "title": "Add all missing parameters", + "command": "azurerm-vscode-tools.codeAction.addAllMissingParameters", + "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": "Add missing required parameters", + "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", @@ -285,10 +315,15 @@ { "command": "azurerm-vscode-tools.insertResource", "when": "never" + }, + { + "command": "azurerm-vscode-tools.openTemplateFile", + "when": "never" } ], "editor/context": [ { + "$comment": "============= Template file commands =============", "command": "azurerm-vscode-tools.selectParameterFile", "group": "zzz_arm-template@1", "when": "editorLangId==arm-template" @@ -296,7 +331,8 @@ { "command": "azurerm-vscode-tools.openParameterFile", "group": "zzz_arm-template@2", - "when": "editorLangId==arm-template" + "when": "editorLangId==arm-template", + "$when.comment": "Shows up when it's a template file, but only enabled if there is an associated param file" }, { "command": "azurerm-vscode-tools.sortTemplate", @@ -307,10 +343,18 @@ "command": "azurerm-vscode-tools.insertItem", "when": "editorLangId==arm-template", "group": "zzz_arm-template@4" + }, + { + "$comment": "============= Parameter file commands =============", + "command": "azurerm-vscode-tools.openTemplateFile", + "group": "zzz_arm-params@2", + "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/item/context": [ { + "$comment": "============= Treeview commands =============", "command": "azurerm-vscode-tools.sortTemplate", "when": "azurerm-vscode-tools.template-outline.active == true", "group": "arm-template" @@ -373,6 +417,7 @@ ], "editor/title": [ { + "$comment": "============= Template file commands =============", "command": "azurerm-vscode-tools.selectParameterFile", "group": "zzz_arm-template@1", "when": "editorLangId==arm-template" @@ -380,11 +425,20 @@ { "command": "azurerm-vscode-tools.openParameterFile", "group": "zzz_arm-template@2", - "when": "editorLangId==arm-template" + "when": "editorLangId==arm-template", + "$when.comment": "Shows up when it's a template file, but only enabled if there is an associated param file" + }, + { + "$comment": "============= Parameter file commands =============", + "command": "azurerm-vscode-tools.openTemplateFile", + "group": "zzz_arm-params@2", + "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" } ], "editor/title/context": [ { + "$comment": "============= Template file commands =============", "command": "azurerm-vscode-tools.selectParameterFile", "group": "zzz_arm-template@1", "when": "editorLangId==arm-template" @@ -392,7 +446,15 @@ { "command": "azurerm-vscode-tools.openParameterFile", "group": "zzz_arm-template@2", - "when": "editorLangId==arm-template" + "when": "editorLangId==arm-template", + "$when.comment": "Shows up when it's a template file, but only enabled if there is an associated param file" + }, + { + "$comment": "============= Parameter file commands =============", + "command": "azurerm-vscode-tools.openTemplateFile", + "group": "zzz_arm-params@2", + "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" } ] } @@ -436,7 +498,7 @@ "ts-node": "^7.0.1", "tslint": "^5.20.1", "tslint-microsoft-contrib": "5.0.3", - "typescript": "^3.7.5", + "typescript": "^3.8.3", "vsce": "^1.73.0", "vscode": "^1.1.33", "vscode-azureextensiondev": "^0.2.4", diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 9fe4697d9..00cc59dcc 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -12,7 +12,8 @@ import * as vscode from "vscode"; import { AzureUserInput, callWithTelemetryAndErrorHandling, callWithTelemetryAndErrorHandlingSync, createAzExtOutputChannel, createTelemetryReporter, IActionContext, registerCommand, registerUIExtensionVariables, TelemetryProperties } from "vscode-azureextensionui"; import { uninstallDotnet } from "./acquisition/dotnetAcquisition"; import * as Completion from "./Completion"; -import { configKeys, configPrefix, expressionsDiagnosticsCompletionMessage, expressionsDiagnosticsSource, extensionName, globalStateKeys, languageId } from "./constants"; +import { armTemplateLanguageId, configKeys, configPrefix, expressionsDiagnosticsCompletionMessage, expressionsDiagnosticsSource, extensionName, globalStateKeys } from "./constants"; +import { DeploymentDocument } from "./DeploymentDocument"; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from "./extensionVariables"; import { Histogram } from "./Histogram"; @@ -24,7 +25,10 @@ import * as Json from "./JSON"; import * as language from "./Language"; import { reloadSchemas } from "./languageclient/reloadSchemas"; import { startArmLanguageServer, stopArmLanguageServer } from "./languageclient/startArmLanguageServer"; -import { considerQueryingForParameterFile, findMappedParameterFileForTemplate, getFriendlyPathToParameterFile, openParameterFile, selectParameterFile } from "./parameterFiles"; +import { DeploymentFileMapping } from "./parameterFiles/DeploymentFileMapping"; +import { DeploymentParameters } from "./parameterFiles/DeploymentParameters"; +import { considerQueryingForParameterFile, getFriendlyPathToFile, openParameterFile, openTemplateFile, selectParameterFile } from "./parameterFiles/parameterFiles"; +import { setParameterFileContext } from "./parameterFiles/setParameterFileContext"; import { IReferenceSite, PositionContext } from "./PositionContext"; import { ReferenceList } from "./ReferenceList"; import { resetGlobalState } from "./resetGlobalState"; @@ -32,13 +36,24 @@ import { getPreferredSchema } from "./schemas"; import { getFunctionParamUsage } from "./signatureFormatting"; import { getQuickPickItems, sortTemplate, SortType } from "./sortTemplate"; import { Stopwatch } from "./Stopwatch"; -import { armDeploymentDocumentSelector, mightBeDeploymentTemplate } from "./supported"; +import { mightBeDeploymentParameters, mightBeDeploymentTemplate, templateDocumentSelector, templateOrParameterDocumentSelector } from "./supported"; import { survey } from "./survey"; +import { TemplatePositionContext } from "./TemplatePositionContext"; import * as TLE from "./TLE"; import { JsonOutlineProvider } from "./Treeview"; import { UnrecognizedBuiltinFunctionIssue } from "./UnrecognizedFunctionIssues"; +import { normalizePath } from "./util/normalizePath"; +import { Cancellation } from "./util/throwOnCancel"; +import { onCompletionActivated, toVsCodeCompletionItem } from "./util/toVsCodeCompletionItem"; import { getVSCodeRangeFromSpan } from "./util/vscodePosition"; +interface IErrorsAndWarnings { + errors: language.Issue[]; + warnings: language.Issue[]; +} + +const invalidRenameError = "Only parameters, variables, user namespaces and user functions can be renamed."; + // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }): Promise { @@ -46,6 +61,11 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta ext.reporter = createTelemetryReporter(context); ext.outputChannel = createAzExtOutputChannel(extensionName, configPrefix); ext.ui = new AzureUserInput(context.globalState); + + context.subscriptions.push(ext.completionItemsSpy); + + ext.deploymentFileMapping.setValue(new DeploymentFileMapping(ext.configuration)); + registerUIExtensionVariables(ext); await callWithTelemetryAndErrorHandling('activate', async (actionContext: IActionContext): Promise => { @@ -64,11 +84,12 @@ export function deactivateInternal(): void { export class AzureRMTools { private readonly _diagnosticsCollection: vscode.DiagnosticCollection; - private readonly _deploymentTemplates: Map = new Map(); + private readonly _deploymentDocuments: Map = new Map(); private readonly _filesAskedToUpdateSchemaThisSession: Set = new Set(); private readonly _paramsStatusBarItem: vscode.StatusBarItem; private _areDeploymentTemplateEventsHookedUp: boolean = false; private _diagnosticsVersion: number = 0; + private _mapping: DeploymentFileMapping = ext.deploymentFileMapping.getValue(); // More information can be found about this definition at https://code.visualstudio.com/docs/extensionAPI/vscode-api#DecorationRenderOptions // Several of these properties are CSS properties. More information about those can be found at https://www.w3.org/wiki/CSS/Properties @@ -85,12 +106,16 @@ export class AzureRMTools { } }); + // tslint:disable-next-line:max-func-body-length constructor(context: vscode.ExtensionContext) { const jsonOutline: JsonOutlineProvider = new JsonOutlineProvider(context); ext.jsonOutlineProvider = jsonOutline; context.subscriptions.push(vscode.window.registerTreeDataProvider("azurerm-vscode-tools.template-outline", jsonOutline)); registerCommand("azurerm-vscode-tools.treeview.goto", (_actionContext: IActionContext, range: vscode.Range) => jsonOutline.goToDefinition(range)); + registerCommand("azurerm-vscode-tools.completion-activated", (actionContext: IActionContext, args: object) => { + onCompletionActivated(actionContext, args); + }); registerCommand('azurerm-vscode-tools.uninstallDotnet', async () => { await stopArmLanguageServer(); await uninstallDotnet(); @@ -128,6 +153,20 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.sortTopLevel", async () => { await this.sortTemplate(SortType.TopLevel); }); + registerCommand( + "azurerm-vscode-tools.selectParameterFile", async (actionContext: IActionContext, source?: vscode.Uri) => { + await selectParameterFile(actionContext, this._mapping, source); + }); + registerCommand( + "azurerm-vscode-tools.openParameterFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { + source = source ?? vscode.window.activeTextEditor?.document.uri; + await openParameterFile(this._mapping, source, undefined); + }); + registerCommand( + "azurerm-vscode-tools.openTemplateFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { + source = source ?? vscode.window.activeTextEditor?.document.uri; + await openTemplateFile(this._mapping, source, undefined); + }); registerCommand("azurerm-vscode-tools.insertItem", async (_context: IActionContext, uri?: vscode.Uri, editor?: vscode.TextEditor) => { editor = editor || vscode.window.activeTextEditor; uri = uri || vscode.window.activeTextEditor?.document.uri; @@ -155,9 +194,13 @@ export class AzureRMTools { registerCommand("azurerm-vscode-tools.insertResource", async () => { await this.insertItem(SortType.Resources); }); - registerCommand("azurerm-vscode-tools.selectParameterFile", selectParameterFile); - registerCommand("azurerm-vscode-tools.openParameterFile", openParameterFile); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); + registerCommand("azurerm-vscode-tools.codeAction.addAllMissingParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { + await this.addMissingParameters(actionContext, source, false); + }); + registerCommand("azurerm-vscode-tools.codeAction.addMissingRequiredParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { + await this.addMissingParameters(actionContext, source, true); + }); this._paramsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); ext.context.subscriptions.push(this._paramsStatusBarItem); @@ -165,7 +208,14 @@ export class AzureRMTools { vscode.window.onDidChangeActiveTextEditor(this.onActiveTextEditorChanged, this, context.subscriptions); vscode.workspace.onDidOpenTextDocument(this.onDocumentOpened, this, context.subscriptions); vscode.workspace.onDidChangeTextDocument(this.onDocumentChanged, this, context.subscriptions); - vscode.workspace.onDidChangeConfiguration(this.updateParameterFileInStatusBar, this, context.subscriptions); + vscode.workspace.onDidChangeConfiguration( + async () => { + this._mapping.resetCache(); + // tslint:disable-next-line: no-floating-promises + this.updateEditorState(); + }, + this, + context.subscriptions); this._diagnosticsCollection = vscode.languages.createDiagnosticCollection("azurerm-tools-expressions"); context.subscriptions.push(this._diagnosticsCollection); @@ -173,7 +223,26 @@ export class AzureRMTools { const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; if (activeEditor) { const activeDocument = activeEditor.document; - this.updateDeploymentTemplate(activeDocument); + this.updateOpenedDocument(activeDocument); + } + } + + private async addMissingParameters( + actionContext: IActionContext, + source: vscode.Uri | undefined, + onlyRequiredParameters: boolean + ): Promise { + source = source || vscode.window.activeTextEditor?.document.uri; + const editor = vscode.window.activeTextEditor; + const paramsUri = source || editor?.document.uri; + if (editor && paramsUri && editor.document.uri.fsPath === paramsUri.fsPath) { + let { doc, associatedDoc: template } = await this.getDeploymentDocAndAssociatedDoc(editor.document, Cancellation.cantCancel); + if (doc instanceof DeploymentParameters) { + await doc.addMissingParameters( + editor, + template, + onlyRequiredParameters); + } } } @@ -181,7 +250,7 @@ export class AzureRMTools { editor = editor || vscode.window.activeTextEditor; documentUri = documentUri || editor?.document.uri; if (editor && documentUri && editor.document.uri.fsPath === documentUri.fsPath) { - let deploymentTemplate = this.getDeploymentTemplate(editor.document); + let deploymentTemplate = this.getOpenedDeploymentTemplate(editor.document); await sortTemplate(deploymentTemplate, sortType, editor); } } @@ -190,7 +259,7 @@ export class AzureRMTools { editor = editor || vscode.window.activeTextEditor; documentUri = documentUri || editor?.document.uri; if (editor && documentUri && editor.document.uri.fsPath === documentUri.fsPath) { - let deploymentTemplate = this.getDeploymentTemplate(editor.document); + let deploymentTemplate = this.getOpenedDeploymentTemplate(editor.document); await new InsertItem(ext.ui).insertItem(deploymentTemplate, sortType, editor); } } @@ -202,102 +271,169 @@ export class AzureRMTools { }); } - private getDeploymentTemplate(document: vscode.TextDocument): DeploymentTemplate | undefined { - assert(document); - return this._deploymentTemplates.get(document.uri.toString()); + // Add the deployment doc to our list of opened deployment docs + private setOpenedDeploymentDocument(documentUri: vscode.Uri, deploymentDocument: DeploymentDocument | undefined): void { + assert(documentUri); + const normalizedPath = normalizePath(documentUri); + if (deploymentDocument) { + this._deploymentDocuments.set(normalizedPath, deploymentDocument); + } else { + this._deploymentDocuments.delete(normalizedPath); + } + } + + private getOpenedDeploymentDocument(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentDocument | undefined { + assert(documentOrUri); + const uri = documentOrUri instanceof vscode.Uri ? documentOrUri : documentOrUri.uri; + const normalizedPath = normalizePath(uri); + return this._deploymentDocuments.get(normalizedPath); + } + + private getOpenedDeploymentTemplate(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentTemplate | undefined { + const file = this.getOpenedDeploymentDocument(documentOrUri); + return file instanceof DeploymentTemplate ? file : undefined; + } + + private getOpenedDeploymentParameters(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentParameters | undefined { + const file = this.getOpenedDeploymentDocument(documentOrUri); + return file instanceof DeploymentParameters ? file : undefined; } - private updateDeploymentTemplate(document: vscode.TextDocument): void { - callWithTelemetryAndErrorHandlingSync('updateDeploymentTemplate', (actionContext: IActionContext): void => { + /** + * Analyzes a text document that has been opened, and handles it appropriately if + * it's a deployment template or parameter file + */ + private updateOpenedDocument(textDocument: vscode.TextDocument): void { + // tslint:disable-next-line:no-suspicious-comment + // TODO: refactor + // tslint:disable-next-line:max-func-body-length cyclomatic-complexity + callWithTelemetryAndErrorHandlingSync('updateDeploymentDocument', (actionContext: IActionContext): void => { actionContext.errorHandling.suppressDisplay = true; actionContext.telemetry.suppressIfSuccessful = true; actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.telemetry.properties.fileExt = path.extname(document.fileName); + actionContext.telemetry.properties.fileExt = path.extname(textDocument.fileName); - assert(document); + assert(textDocument); const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; const stopwatch = new Stopwatch(); stopwatch.start(); let treatAsDeploymentTemplate = false; - let isNewlyOpened = false; // As opposed to already opened and simply being made active - const documentPath: string = document.uri.toString(); + let treatAsDeploymentParameters = false; + const documentUri = textDocument.uri; - if (document.languageId === languageId) { + if (textDocument.languageId === armTemplateLanguageId) { // Lang ID is set to arm-template, whether auto or manual, respect the setting treatAsDeploymentTemplate = true; } - let shouldParseFile = treatAsDeploymentTemplate || mightBeDeploymentTemplate(document); - if (shouldParseFile) { - // If the documentUri is not in our dictionary of deployment templates, then we - // know that this document was just opened (as opposed to changed/updated). - // Note that it might have been opened, then closed, then reopened. - if (!this._deploymentTemplates.has(documentPath)) { - isNewlyOpened = true; - } + // If the documentUri is not in our dictionary of deployment templates, then either + // it's not a deployment file, or else this document was just opened (as opposed + // to changed/updated). + // Note that it might have been opened, then closed, then reopened, or it + // might have had its schema changed in the editor to make it a deployment file. + const isNewlyOpened: boolean = !this.getOpenedDeploymentDocument(documentUri); + // Is it a deployment template file? + let shouldParseFile = treatAsDeploymentTemplate || mightBeDeploymentTemplate(textDocument); + if (shouldParseFile) { // Do a full parse - let deploymentTemplate: DeploymentTemplate = new DeploymentTemplate(document.getText(), documentPath); + let deploymentTemplate: DeploymentTemplate = new DeploymentTemplate(textDocument.getText(), documentUri); if (deploymentTemplate.hasArmSchemaUri()) { treatAsDeploymentTemplate = true; } actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; if (treatAsDeploymentTemplate) { - this.ensureDeploymentTemplateEventsHookedUp(); - this._deploymentTemplates.set(documentPath, deploymentTemplate); + this.ensureDeploymentDocumentEventsHookedUp(); + this.setOpenedDeploymentDocument(documentUri, deploymentTemplate); + survey.registerActiveUse(); if (isNewlyOpened) { // A deployment template has been opened (as opposed to having been tabbed to) // Make sure the language ID is set to arm-template - if (document.languageId !== languageId) { + if (textDocument.languageId !== armTemplateLanguageId) { // The document will be reloaded, firing this event again with the new langid - AzureRMTools.setLanguageToArm(document, actionContext); + AzureRMTools.setLanguageToArm(textDocument, actionContext); return; } + } + + // Not waiting for return + // tslint:disable-next-line: no-floating-promises + this.reportDeploymentTemplateErrors(textDocument, deploymentTemplate).then(async (errorsWarnings) => { + if (isNewlyOpened) { + // Telemetry for template opened + if (errorsWarnings) { + this.reportTemplateOpenedTelemetry(textDocument, deploymentTemplate, stopwatch, errorsWarnings); + } + + // No guarantee that active editor is the one we're processing, ignore if not + if (editor && editor.document === textDocument) { + // Are they using an older schema? Ask to update. + // tslint:disable-next-line: no-suspicious-comment + // TODO: Move to separate file + this.considerQueryingForNewerSchema(editor, deploymentTemplate); + + // Is there a possibly-matching params file they might want to associate? + considerQueryingForParameterFile(this._mapping, textDocument); + } + } + }); + } + } - // Telemetry for template opened - // tslint:disable-next-line: no-floating-promises // Don't wait - this.reportTemplateOpenedTelemetry(document, deploymentTemplate, stopwatch); + if (!treatAsDeploymentTemplate) { + // Is it a parameter file? + let shouldParseParameterFile = treatAsDeploymentTemplate || mightBeDeploymentParameters(textDocument); + if (shouldParseParameterFile) { + // Do a full parse + let deploymentParameters: DeploymentParameters = new DeploymentParameters(textDocument.getText(), textDocument.uri); + if (deploymentParameters.hasParametersUri()) { + treatAsDeploymentParameters = true; + } - // No guarantee that active editor is the one we're processing, ignore if not - if (editor && editor.document === document) { - // Are they using an older schema? Ask to update. - // tslint:disable-next-line: no-suspicious-comment - // TODO: Move to separate file - this.considerQueryingForNewerSchema(editor, deploymentTemplate); + // This could theoretically include time for parsing for a deployment template as well but isn't likely + actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; - // Is there a possibly-matching params file they might want to associate? - considerQueryingForParameterFile(document); - } + if (treatAsDeploymentParameters) { + this.ensureDeploymentDocumentEventsHookedUp(); + this.setOpenedDeploymentDocument(documentUri, deploymentParameters); + survey.registerActiveUse(); + + // tslint:disable-next-line: no-floating-promises + this.reportDeploymentParametersErrors(textDocument, deploymentParameters).then(async (errorsWarnings) => { + if (isNewlyOpened && errorsWarnings) { + // A deployment template has been opened (as opposed to having been tabbed to) + + // Telemetry for parameter file opened + await this.reportParameterFileOpenedTelemetry(textDocument, deploymentParameters, stopwatch, errorsWarnings); + } + }); } + } - this.reportDeploymentTemplateErrors(document, deploymentTemplate); - survey.registerActiveUse(); + if (!treatAsDeploymentTemplate && !treatAsDeploymentParameters) { + // If the document is not a deployment file, then we need + // to remove it from our deployment file cache. It doesn't + // matter if the document is a JSON file and was never a + // deployment file, or if the document was a deployment + // file and then was modified to no longer be a deployment + // file (the $schema property changed to not be a + // template/params schema). In either case, we should + // remove it from our cache. + this.closeDeploymentFile(textDocument); } - } - if (!treatAsDeploymentTemplate) { - // If the document is not a deployment template, then we need - // to remove it from our deployment template cache. It doesn't - // matter if the document is a JSON file and was never a - // deployment template, or if the document was a deployment - // template and then was modified to no longer be a deployment - // template (the $schema property changed to not be a - // deployment template schema). In either case, we should - // remove the deployment template from our cache. - this.closeDeploymentTemplate(document); + // tslint:disable-next-line: no-floating-promises + this.updateEditorState(); } - - // tslint:disable-next-line: no-floating-promises - this.updateParameterFileInStatusBar(); }); } private static setLanguageToArm(document: vscode.TextDocument, actionContext: IActionContext): void { - vscode.languages.setTextDocumentLanguage(document, languageId); + vscode.languages.setTextDocumentLanguage(document, armTemplateLanguageId); actionContext.telemetry.properties.switchedToArm = 'true'; actionContext.telemetry.properties.docLangId = document.languageId; @@ -305,23 +441,22 @@ export class AzureRMTools { actionContext.telemetry.suppressIfSuccessful = false; } - private async reportTemplateOpenedTelemetry( + private reportTemplateOpenedTelemetry( document: vscode.TextDocument, deploymentTemplate: DeploymentTemplate, - stopwatch: Stopwatch - ): Promise { + stopwatch: Stopwatch, + errorsWarnings: IErrorsAndWarnings + ): void { // tslint:disable-next-line: restrict-plus-operands const functionsInEachNamespace = deploymentTemplate.topLevelScope.namespaceDefinitions.map(ns => ns.members.length); // tslint:disable-next-line: restrict-plus-operands const totalUserFunctionsCount = functionsInEachNamespace.reduce((sum, count) => sum + count, 0); const issuesHistograph = new Histogram(); - const errors = await deploymentTemplate.errorsPromise; - const warnings = deploymentTemplate.warnings; - for (const error of errors) { + for (const error of errorsWarnings.errors) { issuesHistograph.add(`extErr:${error.kind}`); } - for (const warning of deploymentTemplate.warnings) { + for (const warning of errorsWarnings.warnings) { issuesHistograph.add(`extWarn:${warning.kind}`); } @@ -347,40 +482,103 @@ export class AzureRMTools { userFunctionsCount: totalUserFunctionsCount, multilineStringCount: deploymentTemplate.getMultilineStringCount(), commentCount: deploymentTemplate.getCommentCount(), - extErrorsCount: errors.length, - extWarnCount: warnings.length, - linkedParameterFiles: findMappedParameterFileForTemplate(document.uri) ? 1 : 0 + extErrorsCount: errorsWarnings.errors.length, + extWarnCount: errorsWarnings.warnings.length, + linkedParameterFiles: this._mapping.getParameterFile(document.uri) ? 1 : 0 }); this.logFunctionCounts(deploymentTemplate); this.logResourceUsage(deploymentTemplate); } - private reportDeploymentTemplateErrors(document: vscode.TextDocument, deploymentTemplate: DeploymentTemplate): void { + private async reportParameterFileOpenedTelemetry( + document: vscode.TextDocument, + parameters: DeploymentParameters, + stopwatch: Stopwatch, + errorsWarnings: IErrorsAndWarnings + ): Promise { + const issuesHistograph = new Histogram(); + for (const error of errorsWarnings.errors) { + issuesHistograph.add(`extErr:${error.kind}`); + } + for (const warning of errorsWarnings.warnings) { + issuesHistograph.add(`extWarn:${warning.kind}`); + } + + ext.reporter.sendTelemetryEvent( + "Parameter File Opened", + { + docLangId: document.languageId, + docExtension: path.extname(document.fileName), + schema: parameters.schemaUri ?? "" + }, + { + documentSizeInCharacters: document.getText().length, + parseDurationInMilliseconds: stopwatch.duration.totalMilliseconds, + lineCount: parameters.lineCount, + maxLineLength: parameters.getMaxLineLength(), + paramsCount: parameters.parametersObjectValue?.length ?? 0, + commentCount: parameters.getCommentCount(), + linkedTemplateFiles: this._mapping.getTemplateFile(document.uri) ? 1 : 0, + extErrorsCount: errorsWarnings.errors.length, + extWarnCount: errorsWarnings.warnings.length + }); + } + + private async reportDeploymentDocumentErrors( + textDocument: vscode.TextDocument, + deploymentDocument: DeploymentDocument, + associatedDocument: DeploymentDocument | undefined + ): Promise { // Don't wait // tslint:disable-next-line: no-floating-promises - callWithTelemetryAndErrorHandling('reportDeploymentTemplateErrors', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.suppressIfSuccessful = true; + ++this._diagnosticsVersion; - ++this._diagnosticsVersion; + let errors: language.Issue[] = await deploymentDocument.getErrors(associatedDocument); + const diagnostics: vscode.Diagnostic[] = []; - let parseErrors: language.Issue[] = await deploymentTemplate.errorsPromise; - const diagnostics: vscode.Diagnostic[] = []; + for (const error of errors) { + diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, error, vscode.DiagnosticSeverity.Error)); + } - for (const error of parseErrors) { - diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentTemplate, error, vscode.DiagnosticSeverity.Error)); - } + const warnings = deploymentDocument.getWarnings(); + for (const warning of warnings) { + diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, warning, vscode.DiagnosticSeverity.Warning)); + } - for (const warning of deploymentTemplate.warnings) { - diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentTemplate, warning, vscode.DiagnosticSeverity.Warning)); - } + let completionDiagnostic = this.getCompletedDiagnostic(); + if (completionDiagnostic) { + diagnostics.push(completionDiagnostic); + } - let completionDiagnostic = this.getCompletedDiagnostic(); - if (completionDiagnostic) { - diagnostics.push(completionDiagnostic); - } + this._diagnosticsCollection.set(textDocument.uri, diagnostics); - this._diagnosticsCollection.set(document.uri, diagnostics); + return { errors, warnings }; + } + + private async reportDeploymentTemplateErrors( + textDocument: vscode.TextDocument, + deploymentTemplate: DeploymentTemplate + ): Promise { + return await callWithTelemetryAndErrorHandling('reportDeploymentTemplateErrors', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; + + // tslint:disable-next-line:no-suspicious-comment + // TODO: Associated parameters + const associatedParameters: DeploymentParameters | undefined = undefined; + return await this.reportDeploymentDocumentErrors(textDocument, deploymentTemplate, associatedParameters); + }); + } + + private async reportDeploymentParametersErrors( + textDocument: vscode.TextDocument, + deploymentParameters: DeploymentParameters + ): Promise { + return await callWithTelemetryAndErrorHandling('reportDeploymentParametersErrors', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; + + const template = await this.getOrReadAssociatedTemplate(textDocument.uri, Cancellation.cantCancel); + return await this.reportDeploymentDocumentErrors(textDocument, deploymentParameters, template); }); } @@ -464,7 +662,7 @@ export class AzureRMTools { const editor = await vscode.window.showTextDocument(uri); // The document might have changed since we asked, so find the $schema again - const currentTemplate = new DeploymentTemplate(editor.document.getText(), `current ${deploymentTemplate.documentId}`); + const currentTemplate = new DeploymentTemplate(editor.document.getText(), editor.document.uri); const currentSchemaValue: Json.StringValue | undefined = currentTemplate.schemaValue; if (currentSchemaValue && currentSchemaValue.unquotedValue === previousSchema) { const range = getVSCodeRangeFromSpan(currentTemplate, currentSchemaValue.unquotedSpan); @@ -500,7 +698,7 @@ export class AzureRMTools { * Hook up events related to template files (as opposed to plain JSON files). This is only called when * actual template files are open, to avoid slowing performance when simple JSON files are opened. */ - private ensureDeploymentTemplateEventsHookedUp(): void { + private ensureDeploymentDocumentEventsHookedUp(): void { if (this._areDeploymentTemplateEventsHookedUp) { return; } @@ -511,74 +709,159 @@ export class AzureRMTools { vscode.workspace.onDidCloseTextDocument(this.onDocumentClosed, this, ext.context.subscriptions); const hoverProvider: vscode.HoverProvider = { - provideHover: (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.Hover | undefined => { - return this.onProvideHover(document, position, token); + provideHover: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideHover(document, position, token); } }; - ext.context.subscriptions.push(vscode.languages.registerHoverProvider(armDeploymentDocumentSelector, hoverProvider)); + ext.context.subscriptions.push(vscode.languages.registerHoverProvider(templateDocumentSelector, hoverProvider)); + + // Code actions provider + const codeActionProvider: vscode.CodeActionProvider = { + provideCodeActions: async ( + textDocument: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> => { + return await this.onProvideCodeActions(textDocument, range, context, token); + } + }; + ext.context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + templateOrParameterDocumentSelector, + codeActionProvider, + { + providedCodeActionKinds: [ + vscode.CodeActionKind.QuickFix + ] + } + )); + // tslint:disable-next-line:no-suspicious-comment const completionProvider: vscode.CompletionItemProvider = { - provideCompletionItems: (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.CompletionList | undefined => { - return this.onProvideCompletionItems(document, position, token); + provideCompletionItems: async ( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + context: vscode.CompletionContext + ): Promise => { + return await this.onProvideCompletions(document, position, token); + }, + resolveCompletionItem: (item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.CompletionItem => { + return this.onResolveCompletionItem(item, token); } }; - ext.context.subscriptions.push(vscode.languages.registerCompletionItemProvider(armDeploymentDocumentSelector, completionProvider, "'", "[", ".")); - + ext.context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + templateOrParameterDocumentSelector, + completionProvider, + "'", "[", ".", "(", '"' + )); + + // tslint:disable-next-line:no-suspicious-comment const definitionProvider: vscode.DefinitionProvider = { - provideDefinition: (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.Definition | undefined => { - return this.onProvideDefinition(document, position, token); + provideDefinition: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideDefinition(document, position, token); } }; - ext.context.subscriptions.push(vscode.languages.registerDefinitionProvider(armDeploymentDocumentSelector, definitionProvider)); + ext.context.subscriptions.push( + vscode.languages.registerDefinitionProvider( + templateOrParameterDocumentSelector, + definitionProvider)); const referenceProvider: vscode.ReferenceProvider = { provideReferences: async (document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise => { return this.onProvideReferences(document, position, context, token); } }; - ext.context.subscriptions.push(vscode.languages.registerReferenceProvider(armDeploymentDocumentSelector, referenceProvider)); + ext.context.subscriptions.push(vscode.languages.registerReferenceProvider(templateOrParameterDocumentSelector, referenceProvider)); const signatureHelpProvider: vscode.SignatureHelpProvider = { - provideSignatureHelp: (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.SignatureHelp | undefined => { - return this.onProvideSignatureHelp(document, position, token); + provideSignatureHelp: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideSignatureHelp(document, position, token); } }; - ext.context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(armDeploymentDocumentSelector, signatureHelpProvider, ",", "(", "\n")); + ext.context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(templateDocumentSelector, signatureHelpProvider, ",", "(", "\n")); const renameProvider: vscode.RenameProvider = { provideRenameEdits: async (document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise => { return await this.onProvideRename(document, position, newName, token); + }, + prepareRename: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.prepareRename(document, position, token); } }; - ext.context.subscriptions.push(vscode.languages.registerRenameProvider(armDeploymentDocumentSelector, renameProvider)); + ext.context.subscriptions.push(vscode.languages.registerRenameProvider(templateOrParameterDocumentSelector, renameProvider)); // tslint:disable-next-line:no-floating-promises // Don't wait startArmLanguageServer(); } - private async updateParameterFileInStatusBar(): Promise { - const activeDocument = vscode.window.activeTextEditor?.document; - if (activeDocument) { - const deploymentTemplate = this.getDeploymentTemplate(activeDocument); - if (deploymentTemplate) { - const paramFileUri = findMappedParameterFileForTemplate(activeDocument.uri); - if (paramFileUri) { - const doesParamFileExist = await fse.pathExists(paramFileUri?.fsPath); - let text = `Parameters: ${getFriendlyPathToParameterFile(activeDocument.uri, paramFileUri)}`; - if (!doesParamFileExist) { - text += " $(error) Not found"; + private async updateEditorState(): Promise { + let show = false; + let isTemplateFile = false; + let templateFileHasParamFile = false; + let isParamFile = false; + let paramFileHasTemplateFile = false; + + try { + const activeDocument = vscode.window.activeTextEditor?.document; + if (activeDocument) { + const deploymentTemplate = this.getOpenedDeploymentDocument(activeDocument); + if (deploymentTemplate instanceof DeploymentTemplate) { + show = true; + isTemplateFile = true; + let statusBarText: string; + + const paramFileUri = this._mapping.getParameterFile(activeDocument.uri); + if (paramFileUri) { + templateFileHasParamFile = true; + const doesParamFileExist = await fse.pathExists(paramFileUri?.fsPath); + statusBarText = `Parameters: ${getFriendlyPathToFile(paramFileUri)}`; + if (!doesParamFileExist) { + statusBarText += " $(error) Not found"; + } + } else { + statusBarText = "Select Parameter File..."; } - this._paramsStatusBarItem.text = text; - } else { - this._paramsStatusBarItem.text = "Select Parameter File..."; + + this._paramsStatusBarItem.command = "azurerm-vscode-tools.selectParameterFile"; + this._paramsStatusBarItem.text = statusBarText; + } else if (deploymentTemplate instanceof DeploymentParameters) { + show = true; + isParamFile = true; + let statusBarText: string; + + const templateFileUri = this._mapping.getTemplateFile(activeDocument.uri); + if (templateFileUri) { + paramFileHasTemplateFile = true; + const doesTemplateFileExist = await fse.pathExists(templateFileUri?.fsPath); + statusBarText = `Template file: ${getFriendlyPathToFile(templateFileUri)}`; + if (!doesTemplateFileExist) { + statusBarText += " $(error) Not found"; + } + } else { + statusBarText = "No template file selected"; + } + + this._paramsStatusBarItem.text = statusBarText; } - this._paramsStatusBarItem.command = "azurerm-vscode-tools.selectParameterFile"; + } + } finally { + if (show) { this._paramsStatusBarItem.show(); - return; + } else { + this._paramsStatusBarItem.hide(); } - } - this._paramsStatusBarItem.hide(); + // tslint:disable-next-line: no-floating-promises + setParameterFileContext({ + isTemplateFile, + hasParamFile: templateFileHasParamFile, + isParamFile: isParamFile, + hasTemplateFile: paramFileHasTemplateFile + }); + } } /** @@ -596,7 +879,7 @@ export class AzureRMTools { incorrectArgs?: string; } & TelemetryProperties = actionContext.telemetry.properties; - let issues: language.Issue[] = await deploymentTemplate.errorsPromise; + let issues: language.Issue[] = await deploymentTemplate.getErrors(undefined); // Full function counts const functionCounts: Histogram = deploymentTemplate.getFunctionCounts(); @@ -661,8 +944,8 @@ export class AzureRMTools { return JSON.stringify(array); } - private getVSCodeDiagnosticFromIssue(deploymentTemplate: DeploymentTemplate, issue: language.Issue, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { - const range: vscode.Range = getVSCodeRangeFromSpan(deploymentTemplate, issue.span); + private getVSCodeDiagnosticFromIssue(deploymentDocument: DeploymentDocument, issue: language.Issue, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { + const range: vscode.Range = getVSCodeRangeFromSpan(deploymentDocument, issue.span); const message: string = issue.message; let diagnostic = new vscode.Diagnostic(range, message, severity); diagnostic.source = expressionsDiagnosticsSource; @@ -670,143 +953,270 @@ export class AzureRMTools { return diagnostic; } - private closeDeploymentTemplate(document: vscode.TextDocument): void { + private closeDeploymentFile(document: vscode.TextDocument): void { assert(document); this._diagnosticsCollection.delete(document.uri); - - this._deploymentTemplates.delete(document.uri.toString()); + this.setOpenedDeploymentDocument(document.uri, undefined); } - private onProvideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.Hover | undefined { - const deploymentTemplate = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return callWithTelemetryAndErrorHandlingSync('Hover', (actionContext: IActionContext): vscode.Hover | undefined => { - actionContext.errorHandling.suppressDisplay = true; - const properties = actionContext.telemetry.properties; + private async onProvideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Hover', async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + const properties = actionContext.telemetry.properties; - const context = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); + const cancel = new Cancellation(token, actionContext); + + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(document, cancel); + if (doc) { + const context = doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); const hoverInfo: Hover.HoverInfo | undefined = context.getHoverInfo(); if (hoverInfo) { properties.hoverType = hoverInfo.friendlyType; - const hoverRange: vscode.Range = getVSCodeRangeFromSpan(deploymentTemplate, hoverInfo.span); + const hoverRange: vscode.Range = getVSCodeRangeFromSpan(doc, hoverInfo.span); const hover = new vscode.Hover(hoverInfo.getHoverText(), hoverRange); return hover; } + } - return undefined; - }); - } + return undefined; + }); } - private onProvideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.CompletionList | undefined { - const deploymentTemplate = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return callWithTelemetryAndErrorHandlingSync('provideCompletionItems', (actionContext: IActionContext): vscode.CompletionList | undefined => { - let properties = actionContext.telemetry.properties; - actionContext.telemetry.suppressIfSuccessful = true; - actionContext.errorHandling.suppressDisplay = true; + private async onProvideCompletions(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('provideCompletionItems', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; + actionContext.errorHandling.suppressDisplay = true; - const context: PositionContext = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); - let completionItemArray: Completion.Item[] = context.getCompletionItems(); - const completionItems: vscode.CompletionItem[] = []; - for (const completion of completionItemArray) { - const insertRange: vscode.Range = getVSCodeRangeFromSpan(deploymentTemplate, completion.insertSpan); - - const completionToAdd = new vscode.CompletionItem(completion.name); - completionToAdd.range = insertRange; - completionToAdd.insertText = new vscode.SnippetString(completion.insertText); - completionToAdd.detail = completion.detail; - completionToAdd.documentation = completion.description ? completion.description : undefined; - - switch (completion.kind) { - case Completion.CompletionKind.Function: - completionToAdd.kind = vscode.CompletionItemKind.Function; - break; - - case Completion.CompletionKind.Parameter: - case Completion.CompletionKind.Variable: - completionToAdd.kind = vscode.CompletionItemKind.Variable; - break; - - case Completion.CompletionKind.Property: - completionToAdd.kind = vscode.CompletionItemKind.Field; - break; - - case Completion.CompletionKind.Namespace: - completionToAdd.kind = vscode.CompletionItemKind.Unit; - break; - - default: - assert.fail(`Unrecognized Completion.Type: ${completion.kind}`); - break; - } + const cancel = new Cancellation(token, actionContext); - // Add completion kind to telemetry - properties.completionKind = typeof completionToAdd.kind === "number" ? vscode.CompletionItemKind[completionToAdd.kind] : undefined; + const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); + if (pc) { + const items: Completion.Item[] = pc.getCompletionItems(); + const vsCodeItems = items.map(c => toVsCodeCompletionItem(pc.document, c)); + ext.completionItemsSpy.postCompletionItemsResult(pc.document, items, vsCodeItems); - completionItems.push(completionToAdd); + // vscode requires all spans to include the original position and be on the same line, otherwise + // it ignores it. Verify that here. + for (let item of vsCodeItems) { + assert(item.range, "Completion item doesn't have a range"); + assert(item.range?.contains(position), "Completion item range doesn't include cursor"); + assert(item.range?.isSingleLine, "Completion item range must be a single line"); } - return new vscode.CompletionList(completionItems, true); - }); + return new vscode.CompletionList(vsCodeItems, true); + } + + return undefined; + }); + } + + private onResolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): vscode.CompletionItem { + ext.completionItemsSpy.postCompletionItemResolution(item); + return item; + } + + /** + * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then + * find the appropriate associated document for it + */ + private async getDeploymentDocAndAssociatedDoc( + textDocument: vscode.TextDocument, + cancel: Cancellation + ): Promise<{ doc?: DeploymentDocument; associatedDoc?: DeploymentDocument }> { + cancel.throwIfCancelled(); + + const doc = this.getOpenedDeploymentDocument(textDocument); + if (!doc) { + // No reason to try reading from disk, if it's not in our opened list, + // it can't be the one in the current text document + return {}; + } + + if (doc instanceof DeploymentTemplate) { + const template: DeploymentTemplate = doc; + // It's a template file - find the associated parameter file, if any + let params: DeploymentParameters | undefined; + const paramsUri: vscode.Uri | undefined = this._mapping.getParameterFile(textDocument.uri); + if (paramsUri) { + params = await this.getOrReadTemplateParameters(paramsUri); + cancel.throwIfCancelled(); + } + + return { doc: template, associatedDoc: params }; + } else if (doc instanceof DeploymentParameters) { + const params: DeploymentParameters = doc; + // It's a parameter file - find the associated template file, if any + let template: DeploymentTemplate | undefined; + const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(textDocument.uri); + if (templateUri) { + template = await this.getOrReadDeploymentTemplate(templateUri); + cancel.throwIfCancelled(); + } + + return { doc: params, associatedDoc: template }; + } else { + assert.fail("Unexpected doc type"); + } + } + + /** + * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then + * create the appropriate context for it from the given position + */ + private async getPositionContext(textDocument: vscode.TextDocument, position: vscode.Position, cancel: Cancellation): Promise { + cancel.throwIfCancelled(); + + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); + if (!doc) { + return undefined; + } + + cancel.throwIfCancelled(); + return doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); + } + + /** + * Given a deployment template URI, return the corresponding opened DeploymentTemplate for it. + * If none, create a new one by reading the location from disk + */ + private async getOrReadDeploymentTemplate(uri: vscode.Uri): Promise { + // Is it already opened? + const doc = this.getOpenedDeploymentTemplate(uri); + if (doc) { + return doc; + } + + // Nope, have to read it from disk + const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); + return new DeploymentTemplate(contents, uri); + } + + /** + * Given a parameter file URI, return the corresponding opened DeploymentParameters for it. + * If none, create a new one by reading the location from disk + */ + private async getOrReadTemplateParameters(uri: vscode.Uri): Promise { + // Is it already opened? + const doc = this.getOpenedDeploymentParameters(uri); + if (doc) { + return doc; + } + + // Nope, have to read it from disk + const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); + return new DeploymentParameters(contents, uri); + } + + private async getOrReadAssociatedTemplate(parameterFileUri: vscode.Uri, cancel: Cancellation): Promise { + const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(parameterFileUri); + if (templateUri) { + const template = await this.getOrReadDeploymentTemplate(templateUri); + cancel.throwIfCancelled(); + return template; } + + return undefined; } - private onProvideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.Location | undefined { - const deploymentTemplate: DeploymentTemplate | undefined = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return callWithTelemetryAndErrorHandlingSync('Go To Definition', (actionContext: IActionContext): vscode.Location | undefined => { - let properties = actionContext.telemetry.properties; + private getDocTypeForTelemetry(doc: DeploymentDocument): string { + if (doc instanceof DeploymentTemplate) { + return "template"; + } else if (doc instanceof DeploymentParameters) { + return "parameters"; + } else { + assert.fail("Unexpected doc type"); + } + } + + private async onProvideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Go To Definition', async (actionContext: IActionContext): Promise => { + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); + + if (pc) { + let properties = actionContext.telemetry.properties; actionContext.errorHandling.suppressDisplay = true; + properties.docType = this.getDocTypeForTelemetry(pc.document); - const context: PositionContext = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); - const refInfo = context.getReferenceSiteInfo(); + const refInfo = pc.getReferenceSiteInfo(false); if (refInfo && refInfo.definition.nameValue) { properties.definitionType = refInfo.definition.definitionKind; return new vscode.Location( - vscode.Uri.parse(deploymentTemplate.documentId), - getVSCodeRangeFromSpan(deploymentTemplate, refInfo.definition.nameValue.span) + refInfo.definitionDocument.documentId, + getVSCodeRangeFromSpan(refInfo.definitionDocument, refInfo.definition.nameValue.span) ); } return undefined; - }); - } + } + }); } - private onProvideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): vscode.Location[] | undefined { - const deploymentTemplate: DeploymentTemplate | undefined = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return callWithTelemetryAndErrorHandlingSync('Find References', (actionContext: IActionContext): vscode.Location[] => { - const results: vscode.Location[] = []; - const locationUri: vscode.Uri = vscode.Uri.parse(deploymentTemplate.documentId); - const positionContext: PositionContext = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); - - const references: ReferenceList | undefined = positionContext.getReferences(); + private async onProvideReferences(textDocument: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Find References', async (actionContext: IActionContext): Promise => { + const cancel = new Cancellation(token, actionContext); + const results: vscode.Location[] = []; + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (pc) { + const references: ReferenceList | undefined = pc.getReferences(); if (references && references.length > 0) { actionContext.telemetry.properties.referenceType = references.kind; - for (const span of references.spans) { - const referenceRange: vscode.Range = getVSCodeRangeFromSpan(deploymentTemplate, span); + for (const ref of references.references) { + const locationUri: vscode.Uri = ref.document.documentId; + const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); results.push(new vscode.Location(locationUri, referenceRange)); } } + } - return results; - }); - } + return results; + }); } - private onProvideSignatureHelp(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.SignatureHelp | undefined { - const deploymentTemplate: DeploymentTemplate | undefined = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return callWithTelemetryAndErrorHandlingSync('provideSignatureHelp', (actionContext: IActionContext): vscode.SignatureHelp | undefined => { - actionContext.errorHandling.suppressDisplay = true; + /** + * Provide commands for the given document and range. + * + * @param textDocument The document in which the command was invoked. + * @param range The selector or range for which the command was invoked. This will always be a selection if + * there is a currently active editor. + * @param context Context carrying additional information. + * @param token A cancellation token. + * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty array. + */ + private async onProvideCodeActions( + textDocument: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> { + return await callWithTelemetryAndErrorHandling('Provide code actions', async (actionContext: IActionContext): Promise<(vscode.Command | vscode.CodeAction)[]> => { + actionContext.errorHandling.suppressDisplay = true; + const cancel = new Cancellation(token, actionContext); - const context: PositionContext = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); + if (doc) { + return await doc.getCodeActions(associatedDoc, range, context); + } - let functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = context.getSignatureHelp(); + return []; + }); + } + + private async onProvideSignatureHelp(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('provideSignatureHelp', async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (pc) { + let functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); let signatureHelp: vscode.SignatureHelp | undefined; if (functionSignatureHelp) { @@ -826,23 +1236,69 @@ export class AzureRMTools { } return signatureHelp; - }); - } + } + + return undefined; + }); } - private async onProvideRename(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { - const deploymentTemplate: DeploymentTemplate | undefined = this.getDeploymentTemplate(document); - if (deploymentTemplate) { - return await callWithTelemetryAndErrorHandling('Rename', async () => { - const result: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + /** + * Optional function for resolving and validating a position *before* running rename. The result can + * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol + * which is being renamed - when omitted the text in the returned range is used. + * + * *Note: * This function should throw an error or return a rejected thenable when the provided location + * doesn't allow for a rename. + * + * @param textDocument The document in which rename will be invoked. + * @param position The position at which rename will be invoked. + * @param token A cancellation token. + * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. + */ + private async prepareRename(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('PrepareRename', async (actionContext) => { + actionContext.errorHandling.rethrow = true; + + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (!token.isCancellationRequested && pc) { + // Make sure the kind of item being renamed is valid + const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); + if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { + actionContext.errorHandling.suppressDisplay = true; + throw new Error("Built-in functions cannot be renamed."); + } + + if (referenceSiteInfo) { + // Get the correct span to replace. In particular, this fixes the fact that in JSON the rename + // dialog tends to pick up the entire string of a params/var name, along with quotation marks, + // but we want just the unquoted string + return getVSCodeRangeFromSpan(pc.document, referenceSiteInfo.referenceSpan); + } + + actionContext.errorHandling.suppressDisplay = true; + throw new Error(invalidRenameError); + } + + return undefined; + }); + } + + private async onProvideRename(textDocument: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Rename', async (actionContext) => { + actionContext.errorHandling.rethrow = true; - const context: PositionContext = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); - const referenceSiteInfo: IReferenceSite | undefined = context.getReferenceSiteInfo(); + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (!token.isCancellationRequested && pc) { + // Make sure the kind of item being renamed is valid + const result: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { throw new Error("Built-in functions cannot be renamed."); } - const referenceList: ReferenceList | undefined = context.getReferences(); + const referenceList: ReferenceList | undefined = pc.getReferences(); if (referenceList) { // When trying to rename a parameter or variable reference inside of a TLE, the // textbox that pops up when you press F2 contains more than just the variable @@ -864,19 +1320,17 @@ export class AzureRMTools { // the user is contained in double quotes. Remove those. newName = newName.replace(/^"(.*)"$/, '$1'); - const documentUri: vscode.Uri = vscode.Uri.parse(deploymentTemplate.documentId); - - for (const referenceSpan of referenceList.spans) { - const referenceRange: vscode.Range = getVSCodeRangeFromSpan(deploymentTemplate, referenceSpan); - result.replace(documentUri, referenceRange, newName); + for (const ref of referenceList.references) { + const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); + result.replace(ref.document.documentId, referenceRange, newName); } } else { - throw new Error('Only parameters, variables, user namespaces and user functions can be renamed.'); + throw new Error(invalidRenameError); } return result; - }); - } + } + }); } private onActiveTextEditorChanged(editor: vscode.TextEditor | undefined): void { @@ -887,34 +1341,34 @@ export class AzureRMTools { let activeDocument: vscode.TextDocument | undefined = editor?.document; if (activeDocument) { - if (!this.getDeploymentTemplate(activeDocument)) { - this.updateDeploymentTemplate(activeDocument); + if (!this.getOpenedDeploymentDocument(activeDocument)) { + this.updateOpenedDocument(activeDocument); } } // tslint:disable-next-line: no-floating-promises - this.updateParameterFileInStatusBar(); + this.updateEditorState(); }); } - private onTextSelectionChanged(): void { - callWithTelemetryAndErrorHandlingSync('onTextSelectionChanged', (actionContext: IActionContext): void => { + private async onTextSelectionChanged(): Promise { + await callWithTelemetryAndErrorHandling('onTextSelectionChanged', async (actionContext: IActionContext): Promise => { actionContext.telemetry.properties.isActivationEvent = 'true'; actionContext.errorHandling.suppressDisplay = true; actionContext.telemetry.suppressIfSuccessful = true; let editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; if (editor) { - let deploymentTemplate = this.getDeploymentTemplate(editor.document); - if (deploymentTemplate) { - let position = editor.selection.anchor; - let context = deploymentTemplate.getContextFromDocumentLineAndColumnIndexes(position.line, position.character); - let tleBraceHighlightIndexes: number[] = TLE.BraceHighlighter.getHighlightCharacterIndexes(context); + let position = editor.selection.anchor; + let pc: PositionContext | undefined = + await this.getPositionContext(editor.document, position, Cancellation.cantCancel); + if (pc && pc instanceof TemplatePositionContext) { + let tleBraceHighlightIndexes: number[] = TLE.BraceHighlighter.getHighlightCharacterIndexes(pc); let braceHighlightRanges: vscode.Range[] = []; for (let tleHighlightIndex of tleBraceHighlightIndexes) { - const highlightSpan = new language.Span(tleHighlightIndex + context.jsonTokenStartIndex, 1); - braceHighlightRanges.push(getVSCodeRangeFromSpan(deploymentTemplate, highlightSpan)); + const highlightSpan = new language.Span(tleHighlightIndex + pc.jsonTokenStartIndex, 1); + braceHighlightRanges.push(getVSCodeRangeFromSpan(pc.document, highlightSpan)); } editor.setDecorations(this._braceHighlightDecorationType, braceHighlightRanges); @@ -924,11 +1378,11 @@ export class AzureRMTools { } private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void { - this.updateDeploymentTemplate(event.document); + this.updateOpenedDocument(event.document); } private onDocumentOpened(openedDocument: vscode.TextDocument): void { - this.updateDeploymentTemplate(openedDocument); + this.updateOpenedDocument(openedDocument); } private onDocumentClosed(closedDocument: vscode.TextDocument): void { @@ -937,7 +1391,7 @@ export class AzureRMTools { actionContext.telemetry.suppressIfSuccessful = true; actionContext.errorHandling.suppressDisplay = true; - this.closeDeploymentTemplate(closedDocument); + this.closeDeploymentFile(closedDocument); }); } } diff --git a/src/Completion.ts b/src/Completion.ts index 7b793207f..0bc651094 100644 --- a/src/Completion.ts +++ b/src/Completion.ts @@ -13,59 +13,65 @@ import { IVariableDefinition } from "./VariableDefinition"; */ export class Item { constructor( - private _name: string, - private _insertText: string, - private _insertSpan: language.Span, - private _detail: string, - private _description: string | undefined, - private _type: CompletionKind + public label: string, + public insertText: string, + public insertSpan: language.Span, + public kind: CompletionKind, + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + public detail?: string, + /** + * A human-readable string that represents a doc-comment. + */ + public documention?: string, + public snippetName?: string, + public additionalEdits?: { span: language.Span; insertText: string }[] ) { } public static fromFunctionMetadata(metadata: IFunctionMetadata, replaceSpan: language.Span): Item { // We want to show the fully-qualified name in the completion's title, but we only need to insert the // unqualified name, since the namespace is already there (if any) - let insertText: string = metadata.unqualifiedName; - // CONSIDER: Adding parentheses is wrong if they're already there - if (metadata.maximumArguments === 0) { - // Cursor should go after the parentheses if no args - insertText += "()$0"; - } else { - // ... or between them if there are args - insertText += "($0)"; - } + const insertText: string = metadata.unqualifiedName; + // Note: We do *not* automtically add parentheses after the function name. This actually + // disrupts the normal flow that customers are expecting. Also, this means users will + // need to type "(" themselves, which will then open up the intellisense completion + // for the arguments, which otherwise wouldn't happen. return new Item( metadata.fullName, insertText, replaceSpan, + CompletionKind.Function, `(function) ${metadata.usage}`, // detail - metadata.description, // description - CompletionKind.Function); + metadata.description // documentation + ); } public static fromNamespaceDefinition(namespace: UserFunctionNamespaceDefinition, replaceSpan: language.Span): Item { const name: string = namespace.nameValue.unquotedValue; - let insertText: string = `${name}.$0`; + let insertText: string = `${name}`; return new Item( name, insertText, replaceSpan, + CompletionKind.Parameter, `(namespace) ${name}`, // detail - "User-defined namespace", // description - CompletionKind.Parameter + "User-defined namespace" // documentation ); } public static fromPropertyName(propertyName: string, replaceSpan: language.Span): Item { return new Item( propertyName, - `${propertyName}$0`, + `${propertyName}`, replaceSpan, + CompletionKind.Property, "(property)", // detail // CONSIDER: Add type, default value, etc. - "", // description - CompletionKind.Property + undefined // documentation ); } @@ -73,46 +79,24 @@ export class Item { const name: string = `'${parameter.nameValue.unquotedValue}'`; return new Item( name, - `${name}${includeRightParenthesisInCompletion ? ")" : ""}$0`, + `${name}${includeRightParenthesisInCompletion ? ")" : ""}`, //asdf replaceSpan, + CompletionKind.Parameter, `(parameter)`, // detail // CONSIDER: Add type, default value, etc. from property definition - parameter.description, // description (from property definition's metadata) - CompletionKind.Parameter); + parameter.description // documentation (from property definition's metadata) + ); } public static fromVariableDefinition(variable: IVariableDefinition, replaceSpan: language.Span, includeRightParenthesisInCompletion: boolean): Item { const variableName: string = `'${variable.nameValue.unquotedValue}'`; return new Item( variableName, - `${variableName}${includeRightParenthesisInCompletion ? ")" : ""}$0`, + `${variableName}${includeRightParenthesisInCompletion ? ")" : ""}`, replaceSpan, + CompletionKind.Variable, `(variable)`, // detail - "", // description - CompletionKind.Variable); - } - - public get name(): string { - return this._name; - } - - public get insertText(): string { - return this._insertText; - } - - public get insertSpan(): language.Span { - return this._insertSpan; - } - - public get detail(): string { - return this._detail; - } - - public get description(): string | undefined { - return this._description; - } - - public get kind(): CompletionKind { - return this._type; + undefined // documentation + ); } } @@ -121,5 +105,9 @@ export enum CompletionKind { Parameter = "Parameter", Variable = "Variable", Property = "Property", - Namespace = "Namespace" + Namespace = "Namespace", + + // Parameter file completions + PropertyValue = "PropertyValue", // Parameter from the template file + NewPropertyValue = "NewPropertyValue" // New, unnamed parameter } diff --git a/src/CompletionsSpy.ts b/src/CompletionsSpy.ts new file mode 100644 index 000000000..50bf1ccac --- /dev/null +++ b/src/CompletionsSpy.ts @@ -0,0 +1,41 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { CompletionItem, Event, EventEmitter } from "vscode"; +import { Completion } from "../extension.bundle"; +import { DeploymentDocument } from "./DeploymentDocument"; + +export interface ICompletionsSpyResult { + document: DeploymentDocument; + completionItems: Completion.Item[]; + vsCodeCompletionItems: CompletionItem[]; +} + +export class CompletionsSpy { + private readonly _completionsEmitter: EventEmitter = new EventEmitter(); + private readonly _resolveEmitter: EventEmitter = new EventEmitter(); + + public readonly onCompletionItems: Event = this._completionsEmitter.event; + public readonly onCompletionItemResolved: Event = this._resolveEmitter.event; + + public postCompletionItemsResult( + document: DeploymentDocument, + completionItems: Completion.Item[], + vsCodeCompletionItems: CompletionItem[] + ): void { + this._completionsEmitter.fire({ + document, + completionItems, + vsCodeCompletionItems + }); + } + + public postCompletionItemResolution(item: CompletionItem): void { + this._resolveEmitter.fire(item); + } + + public dispose(): void { + this._completionsEmitter.dispose(); + } +} diff --git a/src/Configuration.ts b/src/Configuration.ts new file mode 100644 index 000000000..50dc231b8 --- /dev/null +++ b/src/Configuration.ts @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { ConfigurationTarget, workspace } from "vscode"; + +export interface IConfiguration { + // tslint:disable-next-line:no-reserved-keywords + get(key: string): T | undefined; + + inspect(key: string): { globalValue?: T } | undefined; + + update(section: string, value: unknown, configurationTarget?: ConfigurationTarget | boolean): Promise; +} + +export class VsCodeConfiguration implements IConfiguration { + public constructor(public readonly section: string) { + } + + // tslint:disable-next-line:no-reserved-keywords + public get(key: string): T | undefined { + return workspace.getConfiguration(this.section).get(key); + } + + public inspect(key: string): { globalValue?: T } | undefined { + return workspace.getConfiguration(this.section).inspect(key); + } + + public async update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Promise { + await workspace.getConfiguration(this.section).update(section, value, configurationTarget); + } +} diff --git a/src/DeploymentDocument.ts b/src/DeploymentDocument.ts new file mode 100644 index 000000000..3b7afe402 --- /dev/null +++ b/src/DeploymentDocument.ts @@ -0,0 +1,172 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { CodeAction, CodeActionContext, Command, Range, Selection, Uri } from "vscode"; +import { CachedValue } from "./CachedValue"; +import { __debugMarkPositionInString, __debugMarkRangeInString } from "./debugMarkStrings"; +import { INamedDefinition } from "./INamedDefinition"; +import * as Json from "./JSON"; +import * as language from "./Language"; +import { PositionContext } from "./PositionContext"; +import { ReferenceList } from "./ReferenceList"; +import { nonNullValue } from "./util/nonNull"; + +/** + * Represents a deployment-related JSON file + */ +export abstract class DeploymentDocument { + // Parse result for the template JSON document as a whole + private _jsonParseResult: Json.ParseResult; + + // The JSON node for the top-level JSON object (if the JSON is not empty or malformed) + private _topLevelValue: Json.ObjectValue | undefined; + + private _schema: CachedValue = new CachedValue(); + + /** + * Constructor + * + * @param _documentText The string text of the document. + * @param _documentId A unique identifier for this document. Usually this will be a URI to the document. + */ + constructor(private _documentText: string, private _documentId: Uri) { + nonNullValue(_documentId, "_documentId"); + + this._jsonParseResult = Json.parse(_documentText); + this._topLevelValue = Json.asObjectValue(this._jsonParseResult.value); + } + + // tslint:disable-next-line:function-name + public _debugShowTextAt(position: number | language.Span): string { + if (position instanceof language.Span) { + return __debugMarkRangeInString(this.documentText, position.startIndex, position.length); + } else { + return __debugMarkPositionInString(this.documentText, position); + } + } + + /** + * Get the document text as a string. + */ + public get documentText(): string { + return this._documentText; + } + + /** + * The unique identifier for this deployment template, which indicates its location + */ + public get documentId(): Uri { + return this._documentId; + } + + // Parse result for the template JSON document as a whole + public get jsonParseResult(): Json.ParseResult { + return this._jsonParseResult; + } + + // The JSON node for the top-level JSON object (if the JSON is not empty or malformed) + public get topLevelValue(): Json.ObjectValue | undefined { + return this._topLevelValue; + } + + public get schemaUri(): string | undefined { + const schema = this.schemaValue; + return schema ? schema.unquotedValue : undefined; + } + + public get schemaValue(): Json.StringValue | undefined { + return this._schema.getOrCacheValue(() => { + const value: Json.ObjectValue | undefined = Json.asObjectValue(this._jsonParseResult.value); + if (value) { + const schema: Json.StringValue | undefined = Json.asStringValue(value.getPropertyValue("$schema")); + if (schema) { + return schema; + } + } + + return undefined; + }); + } + + public getMaxLineLength(): number { + let max = 0; + for (let len of this.jsonParseResult.lineLengths) { + if (len > max) { + max = len; + } + } + + return max; + } + + public getCommentCount(): number { + return this.jsonParseResult.commentCount; + } + + /** + * Get the number of lines that are in the file. + */ + public get lineCount(): number { + return this._jsonParseResult.lineLengths.length; + } + + /** + * Get the maximum column index for the provided line. For the last line in the file, + * the maximum column index is equal to the line length. For every other line in the file, + * the maximum column index is less than the line length. + */ + public getMaxColumnIndex(lineIndex: number): number { + return this._jsonParseResult.getMaxColumnIndex(lineIndex); + } + + /** + * Get the maximum document character index for this deployment template. + */ + public get maxCharacterIndex(): number { + return this._jsonParseResult.maxCharacterIndex; + } + + public abstract getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number, associatedTemplate: DeploymentDocument | undefined): PositionContext; + + public abstract getContextFromDocumentCharacterIndex(documentCharacterIndex: number, associatedTemplate: DeploymentDocument | undefined): PositionContext; + + public getDocumentCharacterIndex(documentLineIndex: number, documentColumnIndex: number): number { + return this._jsonParseResult.getCharacterIndex(documentLineIndex, documentColumnIndex); + } + + public getDocumentPosition(documentCharacterIndex: number): language.Position { + return this._jsonParseResult.getPositionFromCharacterIndex(documentCharacterIndex); + } + + public getJSONTokenAtDocumentCharacterIndex(documentCharacterIndex: number): Json.Token | undefined { + return this._jsonParseResult.getTokenAtCharacterIndex(documentCharacterIndex); + } + + public getJSONValueAtDocumentCharacterIndex(documentCharacterIndex: number, containsBehavior: language.Contains): Json.Value | undefined { + return this._jsonParseResult.getValueAtCharacterIndex(documentCharacterIndex, containsBehavior); + } + + /** + * Find all references in this document to the given named definition (which may or may not be in this document) + */ + public abstract findReferencesToDefinition(definition: INamedDefinition): ReferenceList; + + /** + * Provide commands for the given document and range. + * + * @param associatedDocument The associated document, if any (for a template file, the associated document is a parameter file, + * for a parameter file, the associated document is a template file) + * @param range The selector or range for which the command was invoked. This will always be a selection if + * there is a currently active editor. + * @param context Context carrying additional information. + * @param token A cancellation token. + * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty array. + */ + public abstract async getCodeActions(associatedDocument: DeploymentDocument | undefined, range: Range | Selection, context: CodeActionContext): Promise<(Command | CodeAction)[]>; + + public abstract getErrors(associatedDocument: DeploymentDocument | undefined): Promise; + + public abstract getWarnings(): language.Issue[]; +} diff --git a/src/DeploymentTemplate.ts b/src/DeploymentTemplate.ts index f33ed3013..ad78e7e45 100644 --- a/src/DeploymentTemplate.ts +++ b/src/DeploymentTemplate.ts @@ -2,22 +2,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ---------------------------------------------------------------------------- +import * as assert from 'assert'; +import { CodeAction, CodeActionContext, Command, Range, Selection, Uri } from "vscode"; import { AzureRMAssets, FunctionsMetadata } from "./AzureRMAssets"; -import { CachedPromise } from "./CachedPromise"; import { CachedValue } from "./CachedValue"; import { templateKeys } from "./constants"; +import { DeploymentDocument } from "./DeploymentDocument"; import { Histogram } from "./Histogram"; import { INamedDefinition } from "./INamedDefinition"; import * as Json from "./JSON"; import * as language from "./Language"; import { ParameterDefinition } from "./ParameterDefinition"; -import { PositionContext } from "./PositionContext"; +import { DeploymentParameters } from "./parameterFiles/DeploymentParameters"; import { ReferenceList } from "./ReferenceList"; import { isArmSchema } from "./schemas"; +import { TemplatePositionContext } from "./TemplatePositionContext"; import { ScopeContext, TemplateScope } from "./TemplateScope"; import * as TLE from "./TLE"; import { UserFunctionNamespaceDefinition } from "./UserFunctionNamespaceDefinition"; -import { nonNullOrEmptyValue } from "./util/nonNull"; import { IVariableDefinition, TopLevelCopyBlockVariableDefinition, TopLevelVariableDefinition } from "./VariableDefinition"; import { FindReferencesVisitor } from "./visitors/FindReferencesVisitor"; import { FunctionCountVisitor } from "./visitors/FunctionCountVisitor"; @@ -28,40 +30,25 @@ import { UndefinedParameterAndVariableVisitor } from "./visitors/UndefinedParame import * as UndefinedVariablePropertyVisitor from "./visitors/UndefinedVariablePropertyVisitor"; import * as UnrecognizedFunctionVisitor from "./visitors/UnrecognizedFunctionVisitor"; -export class DeploymentTemplate { - // Parse result for the template JSON document as a whole - private _jsonParseResult: Json.ParseResult; - +export class DeploymentTemplate extends DeploymentDocument { // The top-level parameters and variables (as opposed to those in user functions and deployment resources) private _topLevelScope: TemplateScope; - // The JSON node for the top-level JSON object (if the JSON is not empty or malformed) - private _topLevelValue: Json.ObjectValue | undefined; - // A map from all JSON string value nodes to their cached TLE parse results private _jsonStringValueToTleParseResultMap: CachedValue> = new CachedValue>(); - // Cached errors and warnings in the template - private _errors: CachedPromise = new CachedPromise(); - private _warnings: CachedValue = new CachedValue(); - private _topLevelNamespaceDefinitions: CachedValue = new CachedValue(); private _topLevelVariableDefinitions: CachedValue = new CachedValue(); private _topLevelParameterDefinitions: CachedValue = new CachedValue(); - private _schema: CachedValue = new CachedValue(); - /** * Create a new DeploymentTemplate object. * * @param _documentText The string text of the document. * @param _documentId A unique identifier for this document. Usually this will be a URI to the document. */ - constructor(private _documentText: string, private _documentId: string) { - nonNullOrEmptyValue(_documentId, "_documentId"); - - this._jsonParseResult = Json.parse(_documentText); - this._topLevelValue = Json.asObjectValue(this._jsonParseResult.value); + constructor(documentText: string, documentId: Uri) { + super(documentText, documentId); this._topLevelScope = new TemplateScope( ScopeContext.TopLevel, @@ -75,17 +62,13 @@ export class DeploymentTemplate { return this._topLevelScope; } - public get topLevelValue(): Json.ObjectValue | undefined { - return this._topLevelValue; - } - public hasArmSchemaUri(): boolean { return isArmSchema(this.schemaUri); } public get apiProfile(): string | undefined { - if (this._topLevelValue) { - const apiProfileValue = Json.asStringValue(this._topLevelValue.getPropertyValue(templateKeys.apiProfile)); + if (this.topLevelValue) { + const apiProfileValue = Json.asStringValue(this.topLevelValue.getPropertyValue(templateKeys.apiProfile)); if (apiProfileValue) { return apiProfileValue.unquotedValue; } @@ -141,121 +124,83 @@ export class DeploymentTemplate { }); } - /** - * Get the document text as a string. - */ - public get documentText(): string { - return this._documentText; - } - - /** - * The unique identifier for this deployment template. Usually this will be a URI to the document. - */ - public get documentId(): string { - return this._documentId; - } + public async getErrors(_associatedParameters: DeploymentParameters | undefined): Promise { + // tslint:disable-next-line:typedef + return new Promise(async (resolve, reject) => { + try { + let functions: FunctionsMetadata = AzureRMAssets.getFunctionsMetadata(); + const parseErrors: language.Issue[] = []; - public get schemaUri(): string | undefined { - const schema = this.schemaValue; - return schema ? schema.unquotedValue : undefined; - } + // Loop through each reachable string in the template + this.visitAllReachableStringValues(jsonStringValue => { + //const jsonTokenStartIndex: number = jsonQuotedStringToken.span.startIndex; + const jsonTokenStartIndex = jsonStringValue.span.startIndex; - public get schemaValue(): Json.StringValue | undefined { - return this._schema.getOrCacheValue(() => { - const value: Json.ObjectValue | undefined = Json.asObjectValue(this._jsonParseResult.value); - if (value) { - const schema: Json.StringValue | undefined = Json.asStringValue(value.getPropertyValue("$schema")); - if (schema) { - return schema; - } - } + const tleParseResult: TLE.ParseResult | undefined = this.getTLEParseResultFromJsonStringValue(jsonStringValue); + const expressionScope: TemplateScope = tleParseResult.scope; - return undefined; - }); - } - - public get errorsPromise(): Promise { - return this._errors.getOrCachePromise(async () => { - // tslint:disable-next-line:typedef - return new Promise(async (resolve, reject) => { - try { - let functions: FunctionsMetadata = AzureRMAssets.getFunctionsMetadata(); - const parseErrors: language.Issue[] = []; - - // Loop through each reachable string in the template - this.visitAllReachableStringValues(jsonStringValue => { - //const jsonTokenStartIndex: number = jsonQuotedStringToken.span.startIndex; - const jsonTokenStartIndex = jsonStringValue.span.startIndex; + for (const error of tleParseResult.errors) { + parseErrors.push(error.translate(jsonTokenStartIndex)); + } - const tleParseResult: TLE.ParseResult | undefined = this.getTLEParseResultFromJsonStringValue(jsonStringValue); - const expressionScope: TemplateScope = tleParseResult.scope; + const tleExpression: TLE.Value | undefined = tleParseResult.expression; - for (const error of tleParseResult.errors) { - parseErrors.push(error.translate(jsonTokenStartIndex)); - } - - const tleExpression: TLE.Value | undefined = tleParseResult.expression; + // Undefined parameter/variable references + const tleUndefinedParameterAndVariableVisitor = + UndefinedParameterAndVariableVisitor.visit( + tleExpression, + tleParseResult.scope); + for (const error of tleUndefinedParameterAndVariableVisitor.errors) { + parseErrors.push(error.translate(jsonTokenStartIndex)); + } - // Undefined parameter/variable references - const tleUndefinedParameterAndVariableVisitor = - UndefinedParameterAndVariableVisitor.visit( - tleExpression, - tleParseResult.scope); - for (const error of tleUndefinedParameterAndVariableVisitor.errors) { - parseErrors.push(error.translate(jsonTokenStartIndex)); - } + // Unrecognized function calls + const tleUnrecognizedFunctionVisitor = UnrecognizedFunctionVisitor.UnrecognizedFunctionVisitor.visit(expressionScope, tleExpression, functions); + for (const error of tleUnrecognizedFunctionVisitor.errors) { + parseErrors.push(error.translate(jsonTokenStartIndex)); + } - // Unrecognized function calls - const tleUnrecognizedFunctionVisitor = UnrecognizedFunctionVisitor.UnrecognizedFunctionVisitor.visit(expressionScope, tleExpression, functions); - for (const error of tleUnrecognizedFunctionVisitor.errors) { - parseErrors.push(error.translate(jsonTokenStartIndex)); - } + // Incorrect number of function arguments + const tleIncorrectArgumentCountVisitor = IncorrectFunctionArgumentCountVisitor.IncorrectFunctionArgumentCountVisitor.visit(tleExpression, functions); + for (const error of tleIncorrectArgumentCountVisitor.errors) { + parseErrors.push(error.translate(jsonTokenStartIndex)); + } - // Incorrect number of function arguments - const tleIncorrectArgumentCountVisitor = IncorrectFunctionArgumentCountVisitor.IncorrectFunctionArgumentCountVisitor.visit(tleExpression, functions); - for (const error of tleIncorrectArgumentCountVisitor.errors) { - parseErrors.push(error.translate(jsonTokenStartIndex)); - } + // Undefined variable properties + const tleUndefinedVariablePropertyVisitor = UndefinedVariablePropertyVisitor.UndefinedVariablePropertyVisitor.visit(tleExpression, expressionScope); + for (const error of tleUndefinedVariablePropertyVisitor.errors) { + parseErrors.push(error.translate(jsonTokenStartIndex)); + } + }); - // Undefined variable properties - const tleUndefinedVariablePropertyVisitor = UndefinedVariablePropertyVisitor.UndefinedVariablePropertyVisitor.visit(tleExpression, expressionScope); - for (const error of tleUndefinedVariablePropertyVisitor.errors) { - parseErrors.push(error.translate(jsonTokenStartIndex)); - } - }); - - // ReferenceInVariableDefinitionsVisitor - const deploymentTemplateObject: Json.ObjectValue | undefined = Json.asObjectValue(this.jsonParseResult.value); - if (deploymentTemplateObject) { - const variablesObject: Json.ObjectValue | undefined = Json.asObjectValue(deploymentTemplateObject.getPropertyValue(templateKeys.variables)); - if (variablesObject) { - const referenceInVariablesFinder = new ReferenceInVariableDefinitionsVisitor(this); - variablesObject.accept(referenceInVariablesFinder); - - // Can't call reference() inside variable definitions - for (const referenceSpan of referenceInVariablesFinder.referenceSpans) { - parseErrors.push( - new language.Issue(referenceSpan, "reference() cannot be invoked inside of a variable definition.", language.IssueKind.referenceInVar)); - } + // ReferenceInVariableDefinitionsVisitor + const deploymentTemplateObject: Json.ObjectValue | undefined = Json.asObjectValue(this.jsonParseResult.value); + if (deploymentTemplateObject) { + const variablesObject: Json.ObjectValue | undefined = Json.asObjectValue(deploymentTemplateObject.getPropertyValue(templateKeys.variables)); + if (variablesObject) { + const referenceInVariablesFinder = new ReferenceInVariableDefinitionsVisitor(this); + variablesObject.accept(referenceInVariablesFinder); + + // Can't call reference() inside variable definitions + for (const referenceSpan of referenceInVariablesFinder.referenceSpans) { + parseErrors.push( + new language.Issue(referenceSpan, "reference() cannot be invoked inside of a variable definition.", language.IssueKind.referenceInVar)); } } - - resolve(parseErrors); - } catch (err) { - reject(err); } - }); + + resolve(parseErrors); + } catch (err) { + reject(err); + } }); } - public get warnings(): language.Issue[] { - return this._warnings.getOrCacheValue(() => { - // tslint:disable-next-line: no-suspicious-comment - const unusedParams = this.findUnusedParameters(); - const unusedVars = this.findUnusedVariables(); - const unusedUserFuncs = this.findUnusedUserFunctions(); - return unusedParams.concat(unusedVars).concat(unusedUserFuncs); - }); + public getWarnings(): language.Issue[] { + const unusedParams = this.findUnusedParameters(); + const unusedVars = this.findUnusedVariables(); + const unusedUserFuncs = this.findUnusedUserFunctions(); + return unusedParams.concat(unusedVars).concat(unusedUserFuncs); } // CONSIDER: PERF: findUnused{Variables,Parameters,findUnusedNamespacesAndUserFunctions} are very inefficient} @@ -265,7 +210,7 @@ export class DeploymentTemplate { for (const variableDefinition of this.getTopLevelVariableDefinitions()) { // Variables are only supported at the top level - const variableReferences: ReferenceList = this.findReferences(variableDefinition); + const variableReferences: ReferenceList = this.findReferencesToDefinition(variableDefinition); if (variableReferences.length === 1) { warnings.push( new language.Issue(variableDefinition.nameValue.span, `The variable '${variableDefinition.nameValue.toString()}' is never used.`, language.IssueKind.unusedVar)); @@ -281,7 +226,7 @@ export class DeploymentTemplate { // Top-level parameters for (const parameterDefinition of this.topLevelScope.parameterDefinitions) { const parameterReferences: ReferenceList = - this.findReferences(parameterDefinition); + this.findReferencesToDefinition(parameterDefinition); if (parameterReferences.length === 1) { warnings.push( new language.Issue( @@ -296,7 +241,7 @@ export class DeploymentTemplate { for (const member of ns.members) { for (const parameterDefinition of member.parameterDefinitions) { const parameterReferences: ReferenceList = - this.findReferences(parameterDefinition); + this.findReferencesToDefinition(parameterDefinition); if (parameterReferences.length === 1) { warnings.push( new language.Issue( @@ -318,7 +263,7 @@ export class DeploymentTemplate { for (const ns of this.topLevelScope.namespaceDefinitions) { for (const member of ns.members) { const userFuncReferences: ReferenceList = - this.findReferences(member); + this.findReferencesToDefinition(member); if (userFuncReferences.length === 1) { warnings.push( new language.Issue( @@ -360,7 +305,7 @@ export class DeploymentTemplate { const apiProfileString = `(profile=${this.apiProfile || 'none'})`.toLowerCase(); // Collect all resources used - const resources: Json.ArrayValue | undefined = this._topLevelValue ? Json.asArrayValue(this._topLevelValue.getPropertyValue(templateKeys.resources)) : undefined; + const resources: Json.ArrayValue | undefined = this.topLevelValue ? Json.asArrayValue(this.topLevelValue.getPropertyValue(templateKeys.resources)) : undefined; if (resources) { traverseResources(resources, undefined); } @@ -403,21 +348,6 @@ export class DeploymentTemplate { } } - public getMaxLineLength(): number { - let max = 0; - for (let len of this.jsonParseResult.lineLengths) { - if (len > max) { - max = len; - } - } - - return max; - } - - public getCommentCount(): number { - return this.jsonParseResult.commentCount; - } - public getMultilineStringCount(): number { let count = 0; this.visitAllReachableStringValues(jsonStringValue => { @@ -429,39 +359,12 @@ export class DeploymentTemplate { return count; } - public get jsonParseResult(): Json.ParseResult { - return this._jsonParseResult; - } - - /** - * Get the number of lines that are in the file. - */ - public get lineCount(): number { - return this._jsonParseResult.lineLengths.length; - } - - /** - * Get the maximum column index for the provided line. For the last line in the file, - * the maximum column index is equal to the line length. For every other line in the file, - * the maximum column index is less than the line length. - */ - public getMaxColumnIndex(lineIndex: number): number { - return this._jsonParseResult.getMaxColumnIndex(lineIndex); - } - - /** - * Get the maximum document character index for this deployment template. - */ - public get maxCharacterIndex(): number { - return this._jsonParseResult.maxCharacterIndex; - } - private getTopLevelParameterDefinitions(): ParameterDefinition[] { return this._topLevelParameterDefinitions.getOrCacheValue(() => { const parameterDefinitions: ParameterDefinition[] = []; - if (this._topLevelValue) { - const parameters: Json.ObjectValue | undefined = Json.asObjectValue(this._topLevelValue.getPropertyValue(templateKeys.parameters)); + if (this.topLevelValue) { + const parameters: Json.ObjectValue | undefined = Json.asObjectValue(this.topLevelValue.getPropertyValue(templateKeys.parameters)); if (parameters) { for (const parameter of parameters.properties) { parameterDefinitions.push(new ParameterDefinition(parameter)); @@ -475,8 +378,8 @@ export class DeploymentTemplate { private getTopLevelVariableDefinitions(): IVariableDefinition[] { return this._topLevelVariableDefinitions.getOrCacheValue(() => { - if (this._topLevelValue) { - const variables: Json.ObjectValue | undefined = Json.asObjectValue(this._topLevelValue.getPropertyValue(templateKeys.variables)); + if (this.topLevelValue) { + const variables: Json.ObjectValue | undefined = Json.asObjectValue(this.topLevelValue.getPropertyValue(templateKeys.variables)); if (variables) { const varDefs: IVariableDefinition[] = []; for (let prop of variables.properties) { @@ -543,8 +446,8 @@ export class DeploymentTemplate { // } // ], - if (this._topLevelValue) { - const functionNamespacesArray: Json.ArrayValue | undefined = Json.asArrayValue(this._topLevelValue.getPropertyValue("functions")); + if (this.topLevelValue) { + const functionNamespacesArray: Json.ArrayValue | undefined = Json.asArrayValue(this.topLevelValue.getPropertyValue("functions")); if (functionNamespacesArray) { for (let namespaceElement of functionNamespacesArray.elements) { const namespaceObject = Json.asObjectValue(namespaceElement); @@ -562,30 +465,12 @@ export class DeploymentTemplate { }); } - public getDocumentCharacterIndex(documentLineIndex: number, documentColumnIndex: number): number { - return this._jsonParseResult.getCharacterIndex(documentLineIndex, documentColumnIndex); - } - - public getDocumentPosition(documentCharacterIndex: number): language.Position { - return this._jsonParseResult.getPositionFromCharacterIndex(documentCharacterIndex); + public getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number, associatedTemplate: DeploymentParameters | undefined): TemplatePositionContext { + return TemplatePositionContext.fromDocumentLineAndColumnIndexes(this, documentLineIndex, documentColumnIndex, associatedTemplate); } - public getJSONTokenAtDocumentCharacterIndex(documentCharacterIndex: number): Json.Token | undefined { - return this._jsonParseResult.getTokenAtCharacterIndex(documentCharacterIndex); - } - - public getJSONValueAtDocumentCharacterIndex(documentCharacterIndex: number): Json.Value | undefined { - return this._jsonParseResult.getValueAtCharacterIndex(documentCharacterIndex); - } - - // CONSIDER: Move this to PositionContext since PositionContext depends on DeploymentTemplate - public getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number): PositionContext { - return PositionContext.fromDocumentLineAndColumnIndexes(this, documentLineIndex, documentColumnIndex); - } - - // CONSIDER: Move this to PositionContext since PositionContext depends on DeploymentTemplate - public getContextFromDocumentCharacterIndex(documentCharacterIndex: number): PositionContext { - return PositionContext.fromDocumentCharacterIndex(this, documentCharacterIndex); + public getContextFromDocumentCharacterIndex(documentCharacterIndex: number, associatedTemplate: DeploymentParameters | undefined): TemplatePositionContext { + return TemplatePositionContext.fromDocumentCharacterIndex(this, documentCharacterIndex, associatedTemplate); } /** @@ -605,13 +490,13 @@ export class DeploymentTemplate { return tleParseResult; } - public findReferences(definition: INamedDefinition): ReferenceList { + public findReferencesToDefinition(definition: INamedDefinition): ReferenceList { const result: ReferenceList = new ReferenceList(definition.definitionKind); const functions: FunctionsMetadata = AzureRMAssets.getFunctionsMetadata(); // Add the definition of whatever's being referenced to the list if (definition.nameValue) { - result.add(definition.nameValue.unquotedSpan); + result.add({ document: this, span: definition.nameValue.unquotedSpan }); } // Find and add references that match the definition we're looking for @@ -619,7 +504,7 @@ export class DeploymentTemplate { const tleParseResult: TLE.ParseResult | undefined = this.getTLEParseResultFromJsonStringValue(jsonStringValue); if (tleParseResult.expression) { // tslint:disable-next-line:no-non-null-assertion // Guaranteed by if - const visitor = FindReferencesVisitor.visit(tleParseResult.expression, definition, functions); + const visitor = FindReferencesVisitor.visit(this, tleParseResult.expression, definition, functions); result.addAll(visitor.references.translate(jsonStringValue.span.startIndex)); } }); @@ -628,9 +513,18 @@ export class DeploymentTemplate { } private visitAllReachableStringValues(onStringValue: (stringValue: Json.StringValue) => void): void { - let value = this._topLevelValue; + let value = this.topLevelValue; if (value) { GenericStringVisitor.visit(value, onStringValue); } } + + public async getCodeActions( + associatedDocument: DeploymentDocument | undefined, + range: Range | Selection, + context: CodeActionContext + ): Promise<(Command | CodeAction)[]> { + assert(!associatedDocument || associatedDocument instanceof DeploymentParameters, "Associated document is of the wrong type"); + return []; + } } diff --git a/src/INamedDefinition.ts b/src/INamedDefinition.ts index 01ddaf31e..3bdfa8fe2 100644 --- a/src/INamedDefinition.ts +++ b/src/INamedDefinition.ts @@ -11,6 +11,9 @@ export enum DefinitionKind { Namespace = "Namespace", UserFunction = "UserFunction", BuiltinFunction = "BuiltinFunction", + + // Parameter files + ParameterValue = "ParameterValue", } /** diff --git a/src/JSON.ts b/src/JSON.ts index 5b9aba8a3..118aa1d9b 100644 --- a/src/JSON.ts +++ b/src/JSON.ts @@ -17,6 +17,7 @@ import { CaseInsensitiveMap } from "./CaseInsensitiveMap"; import { assert } from "./fixed_assert"; import * as language from "./Language"; import * as basic from "./Tokenizer"; +import { assertNever } from "./util/assertNever"; import { nonNullValue } from "./util/nonNull"; import * as utilities from "./Utilities"; @@ -34,20 +35,28 @@ export enum ValueKind { * The different types of tokens that can be parsed from a JSON string. */ export enum TokenType { - LeftCurlyBracket, - RightCurlyBracket, - LeftSquareBracket, - RightSquareBracket, - Comma, - Colon, - Whitespace, - QuotedString, - Number, - Boolean, - Literal, - Null, - Comment, - Unrecognized + LeftCurlyBracket = 0, + RightCurlyBracket = 1, + LeftSquareBracket = 2, + RightSquareBracket = 3, + Comma = 4, + Colon = 5, + Whitespace = 6, + QuotedString = 7, + Number = 8, + Boolean = 9, + Literal = 10, + Null = 11, + Comment = 12, + Unrecognized = 13 +} + +export enum Comments { + /** + * Default (parser generally ignores comments) + */ + ignoreCommentTokens, + includeCommentTokens } /** @@ -592,7 +601,7 @@ export abstract class Value { */ export class ObjectValue extends Value { // Last set with the same (case-insensitive) key wins (just like in Azure template deployment) - private _caseInsensitivePropertyMap: CachedValue> = new CachedValue>(); + private _caseInsensitivePropertyMap: CachedValue> = new CachedValue>(); constructor(span: language.Span, private _properties: Property[]) { super(span); @@ -604,16 +613,16 @@ export class ObjectValue extends Value { } /** - * Get the map of property names to property values for this ObjectValue. This mapping is + * Get the map of property names to properties for this ObjectValue. This mapping is * created lazily. */ - private get caseInsensitivePropertyMap(): CaseInsensitiveMap { + private get caseInsensitivePropertyMap(): CaseInsensitiveMap { return this._caseInsensitivePropertyMap.getOrCacheValue(() => { - const caseInsensitivePropertyMap = new CaseInsensitiveMap(); + const caseInsensitivePropertyMap = new CaseInsensitiveMap(); if (this._properties.length > 0) { for (const property of this._properties) { - caseInsensitivePropertyMap.set(property.nameValue.toString(), property.value); + caseInsensitivePropertyMap.set(property.nameValue.toString(), property); } } @@ -637,7 +646,16 @@ export class ObjectValue extends Value { * provided name (case-insensitive), then undefined will be returned. */ public getPropertyValue(propertyName: string): Value | undefined { - const result = this.caseInsensitivePropertyMap.get(propertyName); + const result: Property | undefined = this.caseInsensitivePropertyMap.get(propertyName); + return result ? result.value : undefined; + } + + /** + * Get the property for the provided property name. If no property exists with the + * provided name (case-insensitive), then undefined will be returned. + */ + public getProperty(propertyName: string): Property | undefined { + const result: Property | undefined = this.caseInsensitivePropertyMap.get(propertyName); return result ? result : undefined; } @@ -723,7 +741,7 @@ export class Property extends Value { public get __debugDisplay(): string { // tslint:disable-next-line: prefer-template - return this._name.toString() + ":" + (this.value instanceof Value ? this.value.__debugDisplay : String(this.value)); + return this._name.quotedValue + ":" + (this.value instanceof Value ? this.value.__debugDisplay : String(this.value)); } } @@ -925,12 +943,76 @@ export class ParseResult { return this._commentTokens; } + public get commentTokenCount(): number { + return this._commentTokens.length; + } + public get lineLengths(): number[] { return this._lineLengths; } + // Might or might not make a copy + public getTokens(commentBehavior: Comments): Token[] { + if (commentBehavior === Comments.includeCommentTokens) { + const tokens = this.tokens.concat(this.commentTokens); + tokens.sort((a, b) => a.span.startIndex - b.span.startIndex); + return tokens; + } else { + return this.tokens; + } + } + + // Might or might not make a copy + public getTokensInSpan(span: language.Span, commentsBehavior: Comments): Token[] { + const results: Token[] = []; + const tokens = this.getTokens(commentsBehavior); + const spanStartIndex = span.startIndex; + const spanEndIndex = span.endIndex; + + for (let token of tokens) { + if (token.span.endIndex >= spanStartIndex) { + if (token.span.startIndex > spanEndIndex) { + break; + } + + results.push(token); + } + } + + return results; + } + + public getLastTokenOnLine(line: number, commentBehavior: Comments = Comments.ignoreCommentTokens): Token | undefined { + const startOfLineIndex = this.getCharacterIndex(line, 0); + const lastLine = this.lineLengths.length - 1; + + const tokens = this.getTokens(commentBehavior); + + let lastSeenToken; + + if (line === lastLine) { + // On last line, check the very last token + lastSeenToken = tokens[tokens.length - 1]; + } else { + const nextLineIndex = this.getCharacterIndex(line + 1, 0); + + for (let token of tokens) { + if (token.span.startIndex >= nextLineIndex) { + break; + } + lastSeenToken = token; + } + } + + if (lastSeenToken && lastSeenToken.span.endIndex >= startOfLineIndex) { + return lastSeenToken; + } else { + return undefined; + } + } + /** - * Get the last character index in this JSON parse result. + * Get the highest character index of any line in this JSON parse result. */ public get maxCharacterIndex(): number { let result = 0; @@ -1002,34 +1084,95 @@ export class ParseResult { return maxColumnIndex; } - private getToken(tokenIndex: number): Token { + private static getToken(tokens: Token[], tokenIndex: number): Token { // tslint:disable-next-line:max-line-length - assert(0 <= tokenIndex && tokenIndex < this.tokenCount, `The tokenIndex (${tokenIndex}) must always be between 0 and the token count - 1 (${this.tokenCount - 1}).`); + assert(0 <= tokenIndex && tokenIndex < tokens.length, `The tokenIndex (${tokenIndex}) must always be between 0 and the token count - 1 (${tokens.length - 1}).`); - return this._tokens[tokenIndex]; + return tokens[tokenIndex]; } - private get lastToken(): Token | undefined { - let tokenCount = this.tokenCount; - return tokenCount > 0 ? this.getToken(tokenCount - 1) : undefined; + private static getLastToken(tokens: Token[]): Token | undefined { + // Returns undefined if no tokens + return tokens[tokens.length - 1]; + } + + // Unlike getTokenAtCharacterIndex by itself, also handles the + // case of being at the end of a line comment ("// comment") + public getCommentTokenAtDocumentIndex( + characterIndex: number, + containsBehavior: language.Contains + ): Token | undefined { + // Check if we're inside a comment token + const token = this.getTokenAtCharacterIndex( + characterIndex, + Comments.includeCommentTokens); + if (token?.type === TokenType.Comment) { + switch (containsBehavior) { + case language.Contains.strict: + return token; + + case language.Contains.extended: + assert.fail("language.Contains.extended not implemented here (somewhat unambiguous and not clear it's useful)"); + + case language.Contains.enclosed: + if (token.span.startIndex === characterIndex) { + return undefined; + } + return token; + + default: + assertNever(containsBehavior); + } + } + + // Are we after a line comment on the same line (if we're on the \r or \n after + // a line comment, the enclosing token is a whitespace token) + const line = this.getPositionFromCharacterIndex(characterIndex).line; + const lastTokenOnLineIncludingComments = this.getLastTokenOnLine( + line, + Comments.includeCommentTokens); + if (lastTokenOnLineIncludingComments + && lastTokenOnLineIncludingComments.type === TokenType.Comment + && lastTokenOnLineIncludingComments.toString().startsWith('//') + && lastTokenOnLineIncludingComments.span.startIndex < characterIndex + ) { + return lastTokenOnLineIncludingComments; + } + + return undefined; } /** - * Get the JSON Token that contains the provided characterIndex, if any (e.g. returns undefined if at whitespace) + * Get the JSON Token that contains the provided characterIndex + * if any (returns undefined if at whitespace or comment) */ - public getTokenAtCharacterIndex(characterIndex: number): Token | undefined { + public getTokenAtCharacterIndex( + characterIndex: number, + commentBehavior: Comments = Comments.includeCommentTokens + ): Token | undefined { assert(0 <= characterIndex, `characterIndex (${characterIndex}) cannot be negative.`); - let token: Token | undefined; + const tokens = this.getTokens(commentBehavior); + return ParseResult.getTokenAtCharacterIndex(tokens, characterIndex); + } - if (!!this.lastToken && this.lastToken.span.afterEndIndex === characterIndex) { - token = this.lastToken; + private static getTokenAtCharacterIndex(tokens: Token[], characterIndex: number): Token | undefined { + assert(0 <= characterIndex, `characterIndex (${characterIndex}) cannot be negative.`); + + const tokenCount: number = tokens.length; + const lastToken: Token | undefined = ParseResult.getLastToken(tokens); + + let token: Token | undefined; + // tslint:disable-next-line: strict-boolean-expressions + if (!!lastToken && lastToken.span.afterEndIndex === characterIndex) { + token = lastToken; } else { + // Perform a binary search let minTokenIndex = 0; - let maxTokenIndex = this.tokenCount - 1; + let maxTokenIndex = tokenCount - 1; while (!token && minTokenIndex <= maxTokenIndex) { let midTokenIndex = Math.floor((maxTokenIndex + minTokenIndex) / 2); - let currentToken = this.getToken(midTokenIndex); + let currentToken = ParseResult.getToken(tokens, midTokenIndex); let currentTokenSpan = currentToken.span; if (characterIndex < currentTokenSpan.startIndex) { @@ -1045,29 +1188,32 @@ export class ParseResult { return token; } - public getValueAtCharacterIndex(characterIndex: number): Value | undefined { + public getValueAtCharacterIndex(characterIndex: number, containsBehavior: language.Contains): Value | undefined { assert(0 <= characterIndex, `characterIndex (${characterIndex}) cannot be negative.`); let result: Value | undefined; - // Find the Value at the given character index via a binary search through the value tree - if (this.value && this.value.span.contains(characterIndex, true)) { + // Find the Value at the given character index by starting at the outside and finding the innermost + // child that contains the point. + if (this.value && this.value.span.contains(characterIndex, containsBehavior)) { let current: Value = this.value; while (!result) { const currentValue: Value = current; + // tslint:disable-next-line:no-suspicious-comment + // TODO: This should not depend on knowledge of the various value types' implementations if (currentValue instanceof Property) { - if (currentValue.nameValue.span.contains(characterIndex, true)) { + if (currentValue.nameValue.span.contains(characterIndex, containsBehavior)) { current = currentValue.nameValue; - } else if (currentValue.value && currentValue.value.span.contains(characterIndex, true)) { + } else if (currentValue.value && currentValue.value.span.contains(characterIndex, containsBehavior)) { current = currentValue.value; } } else if (currentValue instanceof ObjectValue) { assert(currentValue.properties); for (const property of currentValue.properties) { assert(property); - if (property.span.contains(characterIndex, true)) { + if (property.span.contains(characterIndex, containsBehavior)) { current = property; } } @@ -1075,7 +1221,7 @@ export class ParseResult { assert(currentValue.elements); for (const element of currentValue.elements) { assert(element); - if (element.span.contains(characterIndex, true)) { + if (element.span.contains(characterIndex, containsBehavior)) { current = element; } } diff --git a/src/Language.ts b/src/Language.ts index acc5904f5..78cd2a97a 100644 --- a/src/Language.ts +++ b/src/Language.ts @@ -3,8 +3,69 @@ // ---------------------------------------------------------------------------- import * as assert from "assert"; +import { assertNever } from "./util/assertNever"; import { nonNullValue } from "./util/nonNull"; +/** + * Determine if the provided index is contained by this span. + * + * If this span started at 3 and had a length of 4, i.e. [3, 7), then all + * indexes between 3 and 6 (inclusive) would be contained. 2 and 7 would + * not be contained. + * + * If includeAfterEndIndex=true, then 3-7 inclusive would be contained. + */ +export enum Contains { + /* If this span starts at 3 and has a length of 10, i.e. [3, 13), then all + * indices between 3 and 13 (inclusive) would be contained. 2 and 14 would + * not be contained. + * + * Example: "{}" + * index 0: { + * index 1: } + * index 2: EOF + * + * contains(0, ContainsType.strict): true + * contains(1, ContainsType.strict): true + * contains(2, ContainsType.strict): false + */ + strict = 0, + + /* If this span starts at 3 and has a length of 10, i.e. [3, 13), then all + * indices between 3 and 10+1 (inclusive) would be contained. 2 and 12 would + * not be contained. + * + * Example: "{}" + * index 0: { + * index 1: } + * index 2: EOF + * + * contains(0, ContainsType.strict): true + * contains(1, ContainsType.strict): true + * contains(2, ContainsType.strict): true + */ + extended = 1, + + /* If this span starts at 3 and has a length of 10, i.e. [3, 13), then all + * indices between 3+1 and 10 (inclusive) would be contained. 3 and 11 would + * not be contained. + * + * Example: "{}" + * index 0: { + * index 1: } + * index 2: EOF + * + * contains(0, ContainsType.strict): false + * contains(1, ContainsType.strict): true + * contains(2, ContainsType.strict): false + * + * This answers the question of whether a point is enclosed by an object + */ + enclosed = 2 +} + +// tslint:disable-next-line:no-suspicious-comment +// TODO: Move Span to separate file /** * A span representing the character indexes that are contained by a JSONToken. */ @@ -12,6 +73,10 @@ export class Span { constructor(private _startIndex: number, private _length: number) { } + public static fromStartAndAfterEnd(startIndex: number, afterEndIndex: number): Span { + return new Span(startIndex, afterEndIndex - startIndex); + } + /** * Get the start index of this span. */ @@ -43,23 +108,20 @@ export class Span { return this._startIndex + this._length; } - /** - * Determine if the provided index is contained by this span. - * - * If this span started at 3 and had a length of 4 ([3, 7)), then all - * indexes between 3 and 6 (inclusive) would be contained. 2 and 7 would - * not be contained. - */ - public contains(index: number, includeAfterEndIndex: boolean = false): boolean { - let result: boolean = this._startIndex <= index; - if (result) { - if (includeAfterEndIndex) { - result = index <= this.afterEndIndex; - } else { - result = index <= this.endIndex; - } + public contains(index: number, containsBehavior: Contains): boolean { + switch (containsBehavior) { + case Contains.strict: + return this._startIndex <= index && index <= this.endIndex; + + case Contains.extended: + return this._startIndex <= index && index <= this.afterEndIndex; + + case Contains.enclosed: + return this._startIndex + 1 <= index && index <= this.endIndex; + + default: + assertNever(containsBehavior); } - return result; } /** @@ -92,6 +154,47 @@ export class Span { } } + /** + * Create a new span that is the intersection of this and a given span. + * If the provided span is undefined, or they do no intersect, undefined will be returned + */ + public intersect(rhs: Span | undefined): Span | undefined { + if (!!rhs) { + // tslint:disable-next-line:no-this-assignment + let lhs: Span = this; + if (rhs.startIndex < this.startIndex) { + [lhs, rhs] = [rhs, lhs]; + } + + // if (lhs.endIndex < rhs.startIndex) { + // return undefined; + // } + + let start = rhs.startIndex; + let afterEnd = (lhs.afterEndIndex < rhs.afterEndIndex) ? lhs.afterEndIndex : rhs.afterEndIndex; + + if (afterEnd >= start) { + return new Span(start, afterEnd - start); + } else { + return undefined; + } + } + + return undefined; + } + + /** + * Create a new span that is the intersection of the given spans. + * If either is undefined, or they do no intersect, undefined will be returned + */ + public static intersect(lhs: Span | undefined, rhs: Span | undefined): Span | undefined { + if (lhs) { + return lhs.intersect(rhs); + } else { + return undefined; + } + } + public translate(movement: number): Span { return movement === 0 ? this : new Span(this._startIndex + movement, this._length); } @@ -99,6 +202,14 @@ export class Span { public toString(): string { return `[${this.startIndex}, ${this.afterEndIndex})`; } + + public extendLeft(extendLeft: number): Span { + return new Span(this.startIndex - extendLeft, this.length + extendLeft); + } + + public extendRight(extendRight: number): Span { + return new Span(this.startIndex, this.length + extendRight); + } } export class Position { @@ -137,7 +248,10 @@ export enum IssueKind { undefinedParam = "undefinedParam", undefinedVar = "undefinedVar", varInUdf = "varInUdf", - undefinedVarProp = "undefinedVarProp" + undefinedVarProp = "undefinedVarProp", + + // Parameter file issues + params_missingRequiredParam = "params_missingRequiredParam", } /** diff --git a/src/PositionContext.ts b/src/PositionContext.ts index eaa418a7c..48b303e90 100644 --- a/src/PositionContext.ts +++ b/src/PositionContext.ts @@ -2,107 +2,97 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ---------------------------------------------------------------------------- -// tslint:disable:max-line-length - -import { Language } from "../extension.bundle"; -import { AzureRMAssets, BuiltinFunctionMetadata } from "./AzureRMAssets"; import { CachedValue } from "./CachedValue"; import * as Completion from "./Completion"; -import { templateKeys } from "./constants"; import { __debugMarkPositionInString } from "./debugMarkStrings"; -import { DeploymentTemplate } from "./DeploymentTemplate"; +import { DeploymentDocument as DeploymentDocument } from "./DeploymentDocument"; import { assert } from './fixed_assert'; import { HoverInfo } from "./Hover"; -import { IFunctionMetadata, IFunctionParameterMetadata } from "./IFunctionMetadata"; import { INamedDefinition } from "./INamedDefinition"; -import { IParameterDefinition } from "./IParameterDefinition"; import * as Json from "./JSON"; import * as language from "./Language"; -import * as Reference from "./ReferenceList"; -import { TemplateScope } from "./TemplateScope"; +import { ReferenceList } from "./ReferenceList"; import * as TLE from "./TLE"; -import { UserFunctionDefinition } from "./UserFunctionDefinition"; -import { UserFunctionMetadata } from "./UserFunctionMetadata"; -import { UserFunctionNamespaceDefinition } from "./UserFunctionNamespaceDefinition"; +import { InitializeBeforeUse } from "./util/InitializeBeforeUse"; import { nonNullValue } from "./util/nonNull"; -import { IVariableDefinition } from "./VariableDefinition"; -/** - * Information about the TLE expression (if position is at an expression string) - */ -class TleInfo implements ITleInfo { - public constructor( - public readonly tleParseResult: TLE.ParseResult, - public readonly tleCharacterIndex: number, - public readonly tleValue: TLE.Value | undefined, - public readonly scope: TemplateScope - ) { - } +export enum ReferenceSiteKind { + definition = "definition", + reference = "reference" } /** - * Information about a reference site (function call, parameter reference, etc.) + * Information about a reference site (function call, parameter reference, etc.), or to + * a definition itself (function definition, parameter definition, etc.) */ export interface IReferenceSite { + referenceKind: ReferenceSiteKind; + /** - * Where the reference occurs in the template + * Where the reference or definition occurs in the template */ - referenceSpan: Language.Span; + referenceSpan: language.Span; + + /** + * The document that contains the reference + */ + referenceDocument: DeploymentDocument; /** * The definition that the reference refers to */ definition: INamedDefinition; + + /** + * The document that contains the definition + */ + definitionDocument: DeploymentDocument; } /** - * Represents a position inside the snapshot of a deployment template, plus all related information - * that can be parsed and analyzed about it + * Represents a position inside the snapshot of a deployment parameter file, plus all related information + * that can be parsed and analyzed about it from that position. */ -export class PositionContext { - private _deploymentTemplate: DeploymentTemplate; - private _givenDocumentPosition?: language.Position; - private _documentPosition: CachedValue = new CachedValue(); - private _givenDocumentCharacterIndex?: number; - private _documentCharacterIndex: CachedValue = new CachedValue(); +export abstract class PositionContext { + private _documentPosition: InitializeBeforeUse = new InitializeBeforeUse(); + private _documentCharacterIndex: InitializeBeforeUse = new InitializeBeforeUse(); private _jsonToken: CachedValue = new CachedValue(); private _jsonValue: CachedValue = new CachedValue(); - private _tleInfo: CachedValue = new CachedValue(); - private constructor(deploymentTemplate: DeploymentTemplate) { - this._deploymentTemplate = deploymentTemplate; + protected constructor(private _document: DeploymentDocument, private _associatedDocument: DeploymentDocument | undefined) { + nonNullValue(this._document, "document"); } - public static fromDocumentLineAndColumnIndexes(deploymentTemplate: DeploymentTemplate, documentLineIndex: number, documentColumnIndex: number): PositionContext { - nonNullValue(deploymentTemplate, "deploymentTemplate"); + protected initFromDocumentLineAndColumnIndices(documentLineIndex: number, documentColumnIndex: number): void { nonNullValue(documentLineIndex, "documentLineIndex"); assert(documentLineIndex >= 0, "documentLineIndex cannot be negative"); - assert(documentLineIndex < deploymentTemplate.lineCount, `documentLineIndex (${documentLineIndex}) cannot be greater than or equal to the deployment template's line count (${deploymentTemplate.lineCount})`); + assert(documentLineIndex < this._document.lineCount, `documentLineIndex (${documentLineIndex}) cannot be greater than or equal to the deployment template's line count (${this._document.lineCount})`); nonNullValue(documentColumnIndex, "documentColumnIndex"); assert(documentColumnIndex >= 0, "documentColumnIndex cannot be negative"); - assert(documentColumnIndex <= deploymentTemplate.getMaxColumnIndex(documentLineIndex), `documentColumnIndex (${documentColumnIndex}) cannot be greater than the line's maximum index (${deploymentTemplate.getMaxColumnIndex(documentLineIndex)})`); - - let context = new PositionContext(deploymentTemplate); - context._givenDocumentPosition = new language.Position(documentLineIndex, documentColumnIndex); - return context; + assert(documentColumnIndex <= this._document.getMaxColumnIndex(documentLineIndex), `documentColumnIndex (${documentColumnIndex}) cannot be greater than the line's maximum index (${this._document.getMaxColumnIndex(documentLineIndex)})`); + this._documentPosition.setValue(new language.Position(documentLineIndex, documentColumnIndex)); + this._documentCharacterIndex.setValue(this._document.getDocumentCharacterIndex(documentLineIndex, documentColumnIndex)); } - public static fromDocumentCharacterIndex(deploymentTemplate: DeploymentTemplate, documentCharacterIndex: number): PositionContext { - nonNullValue(deploymentTemplate, "deploymentTemplate"); + + protected initFromDocumentCharacterIndex(documentCharacterIndex: number): void { nonNullValue(documentCharacterIndex, "documentCharacterIndex"); assert(documentCharacterIndex >= 0, "documentCharacterIndex cannot be negative"); - assert(documentCharacterIndex <= deploymentTemplate.maxCharacterIndex, `documentCharacterIndex (${documentCharacterIndex}) cannot be greater than the maximum character index (${deploymentTemplate.maxCharacterIndex})`); + assert(documentCharacterIndex <= this._document.maxCharacterIndex, `documentCharacterIndex (${documentCharacterIndex}) cannot be greater than the maximum character index (${this._document.maxCharacterIndex})`); + + this._documentCharacterIndex.setValue(documentCharacterIndex); + this._documentPosition.setValue(this._document.getDocumentPosition(documentCharacterIndex)); + } - let context = new PositionContext(deploymentTemplate); - context._givenDocumentCharacterIndex = documentCharacterIndex; - return context; + public get document(): DeploymentDocument { + return this._document; } /** * Convenient way of seeing what this object represents in the debugger, shouldn't be used for production code */ public get __debugDisplay(): string { - let docText: string = this._deploymentTemplate.documentText; + let docText: string = this._document.documentText; return __debugMarkPositionInString(docText, this.documentCharacterIndex, ""); } @@ -110,18 +100,12 @@ export class PositionContext { * Convenient way of seeing what this object represents in the debugger, shouldn't be used for production code */ public get __debugFullDisplay(): string { - let docText: string = this._deploymentTemplate.documentText; + let docText: string = this._document.documentText; return __debugMarkPositionInString(docText, this.documentCharacterIndex, "", Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); } public get documentPosition(): language.Position { - return this._documentPosition.getOrCacheValue(() => { - if (this._givenDocumentPosition) { - return this._givenDocumentPosition; - } else { - return this._deploymentTemplate.getDocumentPosition(this.documentCharacterIndex); - } - }); + return this._documentPosition.getValue(); } public get documentLineIndex(): number { @@ -133,24 +117,19 @@ export class PositionContext { } public get documentCharacterIndex(): number { - return this._documentCharacterIndex.getOrCacheValue(() => { - if (typeof this._givenDocumentCharacterIndex === "number") { - return this._givenDocumentCharacterIndex; - } else { - return this._deploymentTemplate.getDocumentCharacterIndex(this.documentLineIndex, this.documentColumnIndex); - } - }); + return this._documentCharacterIndex.getValue(); } public get jsonToken(): Json.Token | undefined { return this._jsonToken.getOrCacheValue(() => { - return this._deploymentTemplate.getJSONTokenAtDocumentCharacterIndex(this.documentCharacterIndex); + return this._document.getJSONTokenAtDocumentCharacterIndex(this.documentCharacterIndex); }); } + // NOTE: Includes character after end index public get jsonValue(): Json.Value | undefined { return this._jsonValue.getOrCacheValue(() => { - return this._deploymentTemplate.getJSONValueAtDocumentCharacterIndex(this.documentCharacterIndex); + return this._document.getJSONValueAtDocumentCharacterIndex(this.documentCharacterIndex, language.Contains.extended); }); } @@ -160,28 +139,6 @@ export class PositionContext { return this.jsonToken!.span.startIndex; } - /** - * Retrieves TleInfo for the current position if it's inside a string - */ - public get tleInfo(): TleInfo | undefined { - return this._tleInfo.getOrCacheValue(() => { - //const tleParseResult = this._deploymentTemplate.getTLEParseResultFromJSONToken(this.jsonToken); - const jsonToken = this.jsonToken; - if ( - jsonToken - && jsonToken.type === Json.TokenType.QuotedString - && this.jsonValue - && this.jsonValue instanceof Json.StringValue - ) { - const tleParseResult = this._deploymentTemplate.getTLEParseResultFromJsonStringValue(this.jsonValue); - const tleCharacterIndex = this.documentCharacterIndex - this.jsonTokenStartIndex; - const tleValue = tleParseResult.getValueAtCharacterIndex(tleCharacterIndex); - return new TleInfo(tleParseResult, tleCharacterIndex, tleValue, tleParseResult.scope); - } - return undefined; - }); - } - public get emptySpanAtDocumentCharacterIndex(): language.Span { return new language.Span(this.documentCharacterIndex, 0); } @@ -190,586 +147,51 @@ export class PositionContext { * If this position is inside an expression, inside a reference to an interesting function/parameter/etc, then * return an object with information about this reference and the corresponding definition */ - public getReferenceSiteInfo(): IReferenceSite | undefined { - const tleInfo = this.tleInfo; - if (tleInfo) { - const scope = tleInfo.scope; - const tleCharacterIndex = tleInfo.tleCharacterIndex; - - const tleFuncCall: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleInfo.tleValue); - if (tleFuncCall) { - if (tleFuncCall.namespaceToken && tleFuncCall.namespaceToken.span.contains(tleCharacterIndex)) { - // Inside the namespace of a user-function reference - const ns = tleFuncCall.namespaceToken.stringValue; - const nsDefinition = scope.getFunctionNamespaceDefinition(ns); - if (nsDefinition) { - const referenceSpan: language.Span = tleFuncCall.namespaceToken.span.translate(this.jsonTokenStartIndex); - return { definition: nsDefinition, referenceSpan }; - } - } else if (tleFuncCall.nameToken && tleFuncCall.nameToken.span.contains(tleCharacterIndex)) { - if (tleFuncCall.namespaceToken) { - // Inside the name of a user-function reference - const ns = tleFuncCall.namespaceToken.stringValue; - const name = tleFuncCall.nameToken.stringValue; - const nsDefinition = scope.getFunctionNamespaceDefinition(ns); - const userFunctiondefinition = scope.getUserFunctionDefinition(ns, name); - if (nsDefinition && userFunctiondefinition) { - const referenceSpan: language.Span = tleFuncCall.nameToken.span.translate(this.jsonTokenStartIndex); - return { definition: userFunctiondefinition, referenceSpan }; - } - } else { - // Inside a reference to a built-in function - const functionMetadata: BuiltinFunctionMetadata | undefined = AzureRMAssets.getFunctionMetadataFromName(tleFuncCall.nameToken.stringValue); - if (functionMetadata) { - const referenceSpan: language.Span = tleFuncCall.nameToken.span.translate(this.jsonTokenStartIndex); - return { definition: functionMetadata, referenceSpan }; - } - } - } - } - - const tleStringValue: TLE.StringValue | undefined = TLE.asStringValue(tleInfo.tleValue); - if (tleStringValue instanceof TLE.StringValue) { - if (tleStringValue.isParametersArgument()) { - // Inside the 'xxx' of a parameters('xxx') reference - const parameterDefinition: IParameterDefinition | undefined = scope.getParameterDefinition(tleStringValue.toString()); - if (parameterDefinition) { - const referenceSpan: language.Span = tleStringValue.getSpan().translate(this.jsonTokenStartIndex); - return { definition: parameterDefinition, referenceSpan }; - } - } else if (tleStringValue.isVariablesArgument()) { - const variableDefinition: IVariableDefinition | undefined = scope.getVariableDefinition(tleStringValue.toString()); - if (variableDefinition) { - // Inside the 'xxx' of a variables('xxx') reference - const referenceSpan: language.Span = tleStringValue.getSpan().translate(this.jsonTokenStartIndex); - return { definition: variableDefinition, referenceSpan }; - } - } - } - } - - return undefined; - } - - public getHoverInfo(): HoverInfo | undefined { - const reference: IReferenceSite | undefined = this.getReferenceSiteInfo(); - if (reference) { - const span = reference.referenceSpan; - const definition = reference.definition; - return new HoverInfo(definition.usageInfo, span); - } - - return undefined; - } - - /** - * Get completion items for our position in the document - */ - public getCompletionItems(): Completion.Item[] { - const tleInfo = this.tleInfo; - if (!tleInfo) { - // No string at this position - return []; - } - - // We're inside a JSON string. It may or may not contain square brackets. - - // The function/string/number/etc at the current position inside the string expression, - // or else the JSON string itself even it's not an expression - const tleValue: TLE.Value | undefined = tleInfo.tleValue; - const scope: TemplateScope = tleInfo.scope; - - if (!tleValue || !tleValue.contains(tleInfo.tleCharacterIndex)) { - // No TLE value here. For instance, expression is empty, or before/after/on the square brackets - if (PositionContext.isInsideSquareBrackets(tleInfo.tleParseResult, tleInfo.tleCharacterIndex)) { - // Inside brackets, so complete with all valid functions and namespaces - const replaceSpan = this.emptySpanAtDocumentCharacterIndex; - const functionCompletions = PositionContext.getMatchingFunctionCompletions(scope, undefined, "", replaceSpan); - const namespaceCompletions = PositionContext.getMatchingNamespaceCompletions(scope, "", replaceSpan); - return functionCompletions.concat(namespaceCompletions); - } else { - return []; - } - - } else if (tleValue instanceof TLE.FunctionCallValue) { - return this.getFunctionCallCompletions(tleValue, tleInfo.tleCharacterIndex, scope); - } else if (tleValue instanceof TLE.StringValue) { - return this.getStringLiteralCompletions(tleValue, tleInfo.tleCharacterIndex, scope); - } else if (tleValue instanceof TLE.PropertyAccess) { - return this.getPropertyAccessCompletions(tleValue, tleInfo.tleCharacterIndex, scope); - } - - return []; - } + public abstract getReferenceSiteInfo(includeDefinition: boolean): IReferenceSite | undefined; /** - * Given position in expression is past the left square bracket and before the right square bracket, - * *or* there is no square bracket yet + * Return all references to the item at the cursor position (both in this document + * and any associated documents). The item may be a definition (such as a parameter definition), or + * it may be a reference to an item defined elsewhere (like a variables('xxx') call). + * @returns undefined if references are not supported at this location, or empty list if supported but none found */ - private static isInsideSquareBrackets(parseResult: TLE.ParseResult, characterIndex: number): boolean { - const leftSquareBracketToken: TLE.Token | undefined = parseResult.leftSquareBracketToken; - const rightSquareBracketToken: TLE.Token | undefined = parseResult.rightSquareBracketToken; - - if (leftSquareBracketToken && leftSquareBracketToken.span.afterEndIndex <= characterIndex && - (!rightSquareBracketToken || characterIndex <= rightSquareBracketToken.span.startIndex)) { - return true; - } - - return false; - } - - /** - * Get completions when we're anywhere inside a string literal - */ - private getStringLiteralCompletions(tleValue: TLE.StringValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { - // Start at index 1 to skip past the opening single-quote. - const prefix: string = tleValue.toString().substring(1, tleCharacterIndex - tleValue.getSpan().startIndex); - - if (tleValue.isParametersArgument()) { - // The string is a parameter name inside a parameters('xxx') function - return this.getMatchingParameterCompletions(prefix, tleValue, tleCharacterIndex, scope); - } else if (tleValue.isVariablesArgument()) { - // The string is a variable name inside a variables('xxx') function - return this.getMatchingVariableCompletions(prefix, tleValue, tleCharacterIndex, scope); - } - - return []; - } - - /** - * Get completions when we're anywhere inside a property access, e.g. "resourceGroup().prop1.prop2" - */ - private getPropertyAccessCompletions(tleValue: TLE.PropertyAccess, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { - const functionSource: TLE.FunctionCallValue | undefined = tleValue.functionSource; - - // Property accesses always start with a function call (might be 'variables'/'parameters') - if (functionSource) { - let propertyPrefix: string = ""; - let replaceSpan: language.Span = this.emptySpanAtDocumentCharacterIndex; - const propertyNameToken: TLE.Token | undefined = tleValue.nameToken; - if (propertyNameToken) { - replaceSpan = propertyNameToken.span.translate(this.jsonTokenStartIndex); - propertyPrefix = propertyNameToken.stringValue.substring(0, tleCharacterIndex - propertyNameToken.span.startIndex).toLowerCase(); - } - - const variableProperty: IVariableDefinition | undefined = scope.getVariableDefinitionFromFunctionCall(functionSource); - const parameterProperty: IParameterDefinition | undefined = scope.getParameterDefinitionFromFunctionCall(functionSource); - const sourcesNameStack: string[] = tleValue.sourcesNameStack; - if (variableProperty) { - // [variables('xxx').prop] - - // Is the variable's value is an object? - const sourceVariableDefinition: Json.ObjectValue | undefined = Json.asObjectValue(variableProperty.value); - if (sourceVariableDefinition) { - return this.getDeepPropertyAccessCompletions( - propertyPrefix, - sourceVariableDefinition, - sourcesNameStack, - replaceSpan); - } - } else if (parameterProperty) { - // [parameters('xxx').prop] - - // Is the parameters's default valuean object? - const parameterDefValue: Json.ObjectValue | undefined = parameterProperty.defaultValue ? Json.asObjectValue(parameterProperty.defaultValue) : undefined; - if (parameterDefValue) { - const sourcePropertyDefinition: Json.ObjectValue | undefined = Json.asObjectValue(parameterDefValue.getPropertyValueFromStack(sourcesNameStack)); - if (sourcePropertyDefinition) { - return this.getDeepPropertyAccessCompletions( - propertyPrefix, - sourcePropertyDefinition, - sourcesNameStack, - replaceSpan); - } - } - } else if (sourcesNameStack.length === 0) { - // [function(...).prop] - - // We don't allow multiple levels of property access - // (resourceGroup().prop1.prop2) on functions other than variables/parameters, - // therefore checking that sourcesNameStack.length === 0 - const functionName: string | undefined = functionSource.name; - - // Don't currently support completions from a user function returning an object, - // so there must be no function namespace - if (functionName && !functionSource.namespaceToken) { - let functionMetadata: BuiltinFunctionMetadata | undefined = AzureRMAssets.getFunctionMetadataFromName(functionName); - if (functionMetadata) { - // Property completion off of a built-in function. Completions will consist of the - // returnValueMembers of the function, if any. - const result: Completion.Item[] = []; - for (const returnValueMember of functionMetadata.returnValueMembers) { - if (propertyPrefix === "" || returnValueMember.toLowerCase().startsWith(propertyPrefix)) { - result.push(PositionContext.createPropertyCompletionItem(returnValueMember, replaceSpan)); - } - } - - return result; - } - } - } - } - - return []; - } - - /** - * Return completions when we're anywhere inside a function call expression - */ - // tslint:disable-next-line: max-func-body-length cyclomatic-complexity // Pretty straightforward, don't think further refactoring is important - private getFunctionCallCompletions(tleValue: TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { - assert(tleValue.getSpan().contains(tleCharacterIndex, true), "Position should be inside the function call, or right after it"); - - const namespaceName: string | undefined = tleValue.namespaceToken ? tleValue.namespaceToken.stringValue : undefined; - // tslint:disable-next-line: strict-boolean-expressions - const namespace: UserFunctionNamespaceDefinition | undefined = (namespaceName && scope.getFunctionNamespaceDefinition(namespaceName)) || undefined; - - // The token (namespace or name) that the user is completing and will be replaced with the user's selection - // If undefined, we're just inserting at the current position, not replacing anything - let tleTokenToComplete: TLE.Token | undefined; - - let completeNamespaces: boolean; - let completeBuiltinFunctions: boolean; - let completeUserFunctions: boolean; - - if (tleValue.nameToken && tleValue.nameToken.span.contains(tleCharacterIndex, true)) { - // The caret is inside the function's name (or a namespace before the period has been typed), so one of - // three possibilities. - tleTokenToComplete = tleValue.nameToken; - - if (namespace) { - // 1) "namespace.function" - // Complete only UDF functions - completeUserFunctions = true; - completeNamespaces = false; - completeBuiltinFunctions = false; - } else { - // 2) "namespace" - // 3) "function" - // Complete built-ins and namespaces - completeNamespaces = true; - completeBuiltinFunctions = true; - completeUserFunctions = false; - } - } else if (namespaceName && tleValue.periodToken && tleValue.periodToken.span.afterEndIndex === tleCharacterIndex) { - // "namespace.function" - // The caret is right after the period between a namespace and a function name, so we will be looking for UDF function completions - - if (!namespace) { - // The given namespace is not defined, so no completions - return []; - } - - tleTokenToComplete = tleValue.nameToken; - completeNamespaces = false; - completeBuiltinFunctions = false; - completeUserFunctions = true; - } else if (tleValue.namespaceToken && tleValue.periodToken && tleValue.namespaceToken.span.contains(tleCharacterIndex, true)) { - // "namespace.function" - // The caret is inside the UDF's namespace (e.g., the namespace and at least a period already exist in the call). - // - // So we want built-in functions or namespaces only - - tleTokenToComplete = tleValue.namespaceToken; - completeNamespaces = true; - completeBuiltinFunctions = true; - completeUserFunctions = false; - - } else if (tleValue.isCallToBuiltinWithName(templateKeys.parameters) && tleValue.argumentExpressions.length === 0) { - // "parameters" or "parameters()" or similar - return this.getMatchingParameterCompletions("", tleValue, tleCharacterIndex, scope); - } else if (tleValue.isCallToBuiltinWithName(templateKeys.variables) && tleValue.argumentExpressions.length === 0) { - // "variables" or "variables()" or similar - return this.getMatchingVariableCompletions("", tleValue, tleCharacterIndex, scope); - } else { - // Anywhere else (e.g. whitespace after function name, or inside the arguments list). - // - // "function ()" - // "function()" - // etc. - // - // Assume the user is starting a new function call and provide all completions at that location; - - tleTokenToComplete = undefined; - completeNamespaces = true; - completeBuiltinFunctions = true; - completeUserFunctions = false; - } - - let replaceSpan: language.Span; - let completionPrefix: string; - - // Figure out the span which will be replaced by the completion - if (tleTokenToComplete) { - const tokenToCompleteStartIndex: number = tleTokenToComplete.span.startIndex; - completionPrefix = tleTokenToComplete.stringValue.substring(0, tleCharacterIndex - tokenToCompleteStartIndex); - if (completionPrefix.length === 0) { - replaceSpan = this.emptySpanAtDocumentCharacterIndex; - } else { - replaceSpan = tleTokenToComplete.span.translate(this.jsonTokenStartIndex); - } - } else { - // Nothing getting completed, completion selection will be inserted at current location - replaceSpan = this.emptySpanAtDocumentCharacterIndex; - completionPrefix = ""; - } - - assert(completeBuiltinFunctions || completeUserFunctions || completeNamespaces, "Should be completing something"); - let builtinCompletions: Completion.Item[] = []; - let userFunctionCompletions: Completion.Item[] = []; - let namespaceCompletions: Completion.Item[] = []; - - if (completeBuiltinFunctions || completeUserFunctions) { - if (completeUserFunctions && namespace) { - userFunctionCompletions = PositionContext.getMatchingFunctionCompletions(scope, namespace, completionPrefix, replaceSpan); - } - if (completeBuiltinFunctions) { - builtinCompletions = PositionContext.getMatchingFunctionCompletions(scope, undefined, completionPrefix, replaceSpan); - } - } - if (completeNamespaces) { - namespaceCompletions = PositionContext.getMatchingNamespaceCompletions(scope, completionPrefix, replaceSpan); - } - - return builtinCompletions.concat(namespaceCompletions).concat(userFunctionCompletions); - } - - private getDeepPropertyAccessCompletions(propertyPrefix: string, variableOrParameterDefinition: Json.ObjectValue, sourcesNameStack: string[], replaceSpan: language.Span): Completion.Item[] { - const result: Completion.Item[] = []; - - const sourcePropertyDefinitionObject: Json.ObjectValue | undefined = Json.asObjectValue(variableOrParameterDefinition.getPropertyValueFromStack(sourcesNameStack)); - if (sourcePropertyDefinitionObject) { - let matchingPropertyNames: string[]; - if (!propertyPrefix) { - matchingPropertyNames = sourcePropertyDefinitionObject.propertyNames; - } else { - // We need to ignore casing when creating completions - const propertyPrefixLC = propertyPrefix.toLowerCase(); - - matchingPropertyNames = []; - for (const propertyName of sourcePropertyDefinitionObject.propertyNames) { - if (propertyName.toLowerCase().startsWith(propertyPrefixLC)) { - matchingPropertyNames.push(propertyName); - } - } - } - - for (const matchingPropertyName of matchingPropertyNames) { - result.push(PositionContext.createPropertyCompletionItem(matchingPropertyName, replaceSpan)); - } + public getReferences(): ReferenceList | undefined { + // Find what's at the cursor position + // References in this document + const references: ReferenceList | undefined = this.getReferencesCore(); + if (!references) { + return undefined; } - return result; - } - - private static createPropertyCompletionItem(propertyName: string, replaceSpan: language.Span): Completion.Item { - return Completion.Item.fromPropertyName(propertyName, replaceSpan); - } - - // Returns undefined if references are not supported at this location. - // Returns empty list if supported but none found - public getReferences(): Reference.ReferenceList | undefined { - const tleInfo = this.tleInfo; - if (tleInfo) { // If we're inside a string (whether an expression or not) - const refInfo = this.getReferenceSiteInfo(); + if (this._associatedDocument) { + // References/definitions in the associated document + const refInfo = this.getReferenceSiteInfo(true); if (refInfo) { - return this._deploymentTemplate.findReferences(refInfo.definition); - } - - // Handle when we're directly on the name of a parameter/variable/etc definition (as opposed to a reference) - const jsonStringValue: Json.StringValue | undefined = Json.asStringValue(this.jsonValue); - if (jsonStringValue) { - const unquotedString = jsonStringValue.unquotedValue; - const scope = tleInfo.scope; - - // Is it a parameter definition? - const parameterDefinition: IParameterDefinition | undefined = scope.getParameterDefinition(unquotedString); - if (parameterDefinition && parameterDefinition.nameValue === jsonStringValue) { - return this._deploymentTemplate.findReferences(parameterDefinition); - } - - // Is it a variable definition? - const variableDefinition: IVariableDefinition | undefined = scope.getVariableDefinition(unquotedString); - if (variableDefinition && variableDefinition.nameValue === jsonStringValue) { - return this._deploymentTemplate.findReferences(variableDefinition); - } - - // Is it a user namespace definition? - const namespaceDefinition: UserFunctionNamespaceDefinition | undefined = scope.getFunctionNamespaceDefinition(unquotedString); - if (namespaceDefinition && namespaceDefinition.nameValue === jsonStringValue) { - return this._deploymentTemplate.findReferences(namespaceDefinition); - } - - // Is it a user function definition inside any namespace? - for (let ns of scope.namespaceDefinitions) { - const userFunctionDefinition: UserFunctionDefinition | undefined = scope.getUserFunctionDefinition(ns.nameValue.unquotedValue, unquotedString); - if (userFunctionDefinition && userFunctionDefinition.nameValue === jsonStringValue) { - return this._deploymentTemplate.findReferences(userFunctionDefinition); - } - } + const templateReferences = this._associatedDocument.findReferencesToDefinition(refInfo.definition); + references.addAll(templateReferences); } } - - return undefined; - } - - public getSignatureHelp(): TLE.FunctionSignatureHelp | undefined { - const tleValue: TLE.Value | undefined = this.tleInfo && this.tleInfo.tleValue; - if (this.tleInfo && tleValue) { - let functionToHelpWith: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleValue); - if (!functionToHelpWith) { - functionToHelpWith = TLE.asFunctionCallValue(tleValue.parent); - } - - if (functionToHelpWith && functionToHelpWith.name) { - let functionMetadata: IFunctionMetadata | undefined; - - if (functionToHelpWith.namespaceToken) { - // Call to user-defined function - const namespace: string = functionToHelpWith.namespaceToken.stringValue; - const name: string | undefined = functionToHelpWith.name; - const udfDefinition: UserFunctionDefinition | undefined = this.tleInfo.scope.getUserFunctionDefinition(namespace, name); - functionMetadata = udfDefinition ? UserFunctionMetadata.fromDefinition(udfDefinition) : undefined; - } else { - // Call to built-in function - functionMetadata = AzureRMAssets.getFunctionMetadataFromName(functionToHelpWith.name); - } - if (functionMetadata) { - let currentArgumentIndex: number = 0; - - for (const commaToken of functionToHelpWith.commaTokens) { - if (commaToken.span.startIndex < this.tleInfo.tleCharacterIndex) { - ++currentArgumentIndex; - } - } - - const functionMetadataParameters: IFunctionParameterMetadata[] = functionMetadata.parameters; - if (functionMetadataParameters.length > 0 && - functionMetadataParameters.length <= currentArgumentIndex && - functionMetadataParameters[functionMetadataParameters.length - 1].name.endsWith("...")) { - - currentArgumentIndex = functionMetadataParameters.length - 1; - } - - return new TLE.FunctionSignatureHelp(currentArgumentIndex, functionMetadata); - } - } - } - - return undefined; + return references; } /** - * Given a possible namespace name plus a function name prefix and replacement span, return a list - * of completions for functions or namespaces starting with that prefix + * Return all references to the given reference site info in this document + * @returns undefined if references are not supported at this location, or empty list if supported but none found */ - private static getMatchingFunctionCompletions(scope: TemplateScope, namespace: UserFunctionNamespaceDefinition | undefined, functionNamePrefix: string, replaceSpan: language.Span): Completion.Item[] { - let matches: IFunctionMetadata[]; - - if (namespace) { - // User-defined function - matches = scope.findFunctionDefinitionsWithPrefix(namespace, functionNamePrefix).map(fd => UserFunctionMetadata.fromDefinition(fd)); - } else { - // Built-in function - matches = functionNamePrefix === "" ? - AzureRMAssets.getFunctionsMetadata().functionMetadata : - AzureRMAssets.getFunctionMetadataFromPrefix(functionNamePrefix); - } - - return matches.map(m => Completion.Item.fromFunctionMetadata(m, replaceSpan)); - } - - /** - * Given a possible namespace name plus a function name prefix and replacement span, return a list - * of completions for functions or namespaces starting with that prefix - */ - private static getMatchingNamespaceCompletions(scope: TemplateScope, namespacePrefix: string, replaceSpan: language.Span): Completion.Item[] { - const matches: UserFunctionNamespaceDefinition[] = scope.findNamespaceDefinitionsWithPrefix(namespacePrefix); - return matches.map(m => Completion.Item.fromNamespaceDefinition(m, replaceSpan)); - } - - private getMatchingParameterCompletions(prefix: string, tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { - const replaceSpanInfo: ReplaceSpanInfo = this.getReplaceSpanInfo(tleValue, tleCharacterIndex); - - const parameterCompletions: Completion.Item[] = []; - const parameterDefinitionMatches: IParameterDefinition[] = scope.findParameterDefinitionsWithPrefix(prefix); - for (const parameterDefinition of parameterDefinitionMatches) { - parameterCompletions.push(Completion.Item.fromParameterDefinition(parameterDefinition, replaceSpanInfo.replaceSpan, replaceSpanInfo.includeRightParenthesisInCompletion)); - } - return parameterCompletions; - } - - private getMatchingVariableCompletions(prefix: string, tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { - const replaceSpanInfo: ReplaceSpanInfo = this.getReplaceSpanInfo(tleValue, tleCharacterIndex); - - const variableCompletions: Completion.Item[] = []; - const variableDefinitionMatches: IVariableDefinition[] = scope.findVariableDefinitionsWithPrefix(prefix); - for (const variableDefinition of variableDefinitionMatches) { - variableCompletions.push(Completion.Item.fromVariableDefinition(variableDefinition, replaceSpanInfo.replaceSpan, replaceSpanInfo.includeRightParenthesisInCompletion)); - } - return variableCompletions; - } - - private getReplaceSpanInfo(tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number): ReplaceSpanInfo { - let includeRightParenthesisInCompletion: boolean = true; - let replaceSpan: language.Span; - if (tleValue instanceof TLE.StringValue) { - const stringSpan: language.Span = tleValue.getSpan(); - const stringStartIndex: number = stringSpan.startIndex; - const functionValue: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleValue.parent); - - const rightParenthesisIndex: number = tleValue.toString().indexOf(")"); - const rightSquareBracketIndex: number = tleValue.toString().indexOf("]"); - if (rightParenthesisIndex >= 0) { - replaceSpan = new language.Span(stringStartIndex, rightParenthesisIndex + 1); - } else if (rightSquareBracketIndex >= 0) { - replaceSpan = new language.Span(stringStartIndex, rightSquareBracketIndex); - } else if (functionValue && functionValue.rightParenthesisToken && functionValue.argumentExpressions.length === 1) { - replaceSpan = new language.Span(stringStartIndex, functionValue.rightParenthesisToken.span.afterEndIndex - stringStartIndex); - } else { - includeRightParenthesisInCompletion = !!functionValue && functionValue.argumentExpressions.length <= 1; - replaceSpan = stringSpan; - } + protected abstract getReferencesCore(): ReferenceList | undefined; - replaceSpan = replaceSpan.translate(this.jsonTokenStartIndex); - } else { - if (tleValue.rightParenthesisToken) { - replaceSpan = new language.Span( - this.documentCharacterIndex, - tleValue.rightParenthesisToken.span.startIndex - tleCharacterIndex + 1); - } else { - replaceSpan = this.emptySpanAtDocumentCharacterIndex; - } + public getHoverInfo(): HoverInfo | undefined { + const reference: IReferenceSite | undefined = this.getReferenceSiteInfo(false); + if (reference) { + const span = reference.referenceSpan; + const definition = reference.definition; + return new HoverInfo(definition.usageInfo, span); } - return { - includeRightParenthesisInCompletion: includeRightParenthesisInCompletion, - replaceSpan: replaceSpan - }; + return undefined; } -} - -interface ReplaceSpanInfo { - includeRightParenthesisInCompletion: boolean; - replaceSpan: language.Span; -} -interface ITleInfo { - /** - * The parse result of the enclosing string, if we're inside a string (it's with an expression or not) - */ - tleParseResult: TLE.ParseResult; - - /** - * The index inside the enclosing string, if we're inside a string (whether it's an expression or not) - */ - tleCharacterIndex: number; - - /** - * The outermost TLE value enclosing the current position, if we're inside a string - * (whether it's an expression or not). This can undefined if inside square brackets but before - * an expression, etc. - */ - tleValue: TLE.Value | undefined; + public abstract getCompletionItems(): Completion.Item[]; + public abstract getSignatureHelp(): TLE.FunctionSignatureHelp | undefined; } diff --git a/src/ReferenceList.ts b/src/ReferenceList.ts index 263332573..113f4fe04 100644 --- a/src/ReferenceList.ts +++ b/src/ReferenceList.ts @@ -2,44 +2,48 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ---------------------------------------------------------------------------- +import { DeploymentDocument } from "./DeploymentDocument"; import { assert } from "./fixed_assert"; import { DefinitionKind } from "./INamedDefinition"; import * as language from "./Language"; import { nonNullValue } from "./util/nonNull"; +export interface IReference { + span: language.Span; + document: DeploymentDocument; +} + /** * A list of references that have been found. */ export class ReferenceList { - constructor(private _type: DefinitionKind, private _spans: language.Span[] = []) { + constructor(private _type: DefinitionKind, private _refs: IReference[] = []) { nonNullValue(_type, "_type"); - nonNullValue(_spans, "_spans"); + nonNullValue(_refs, "_refs"); } public get length(): number { - return this._spans.length; + return this._refs.length; } - public get spans(): language.Span[] { - return this._spans; + public get references(): IReference[] { + return this._refs; } public get kind(): DefinitionKind { return this._type; } - public add(span: language.Span): void { - assert(span); - - this._spans.push(span); + public add(reference: IReference): void { + this._refs.push(reference); } public addAll(list: ReferenceList): void { assert(list); assert.deepStrictEqual(this._type, list.kind, "Cannot add references from a list of a different reference type."); - for (const span of list.spans) { - this.add(span); + for (const ref of list.references) { + this.add(ref); } } @@ -48,8 +52,11 @@ export class ReferenceList { const result = new ReferenceList(this._type); - for (const span of this._spans) { - result.add(span.translate(movement)); + for (const ref of this.references) { + result.add({ + document: ref.document, + span: ref.span.translate(movement) + }); } return result; diff --git a/src/TLE.ts b/src/TLE.ts index 2710beccf..73eb0f5bb 100644 --- a/src/TLE.ts +++ b/src/TLE.ts @@ -10,12 +10,12 @@ // tslint:disable:max-classes-per-file // Grandfathered in import { templateKeys } from "./constants"; -import { __debugMarkSubstring } from "./debugMarkStrings"; +import { __debugMarkRangeInString } from "./debugMarkStrings"; import { assert } from "./fixed_assert"; import { IFunctionMetadata } from "./IFunctionMetadata"; import * as Json from "./JSON"; import * as language from "./Language"; -import { PositionContext } from "./PositionContext"; +import { TemplatePositionContext } from "./TemplatePositionContext"; import { TemplateScope } from "./TemplateScope"; import * as basic from "./Tokenizer"; import { nonNullValue } from "./util/nonNull"; @@ -23,6 +23,12 @@ import * as Utilities from "./Utilities"; const tleSyntax: language.IssueKind = language.IssueKind.tleSyntax; +export function isTleExpression(unquotedStringValue: string): boolean { + // An expression must start with '[' (no whitespace before), + // not start with '[[', and end with ']' (no whitespace after) + return !!unquotedStringValue.match(/^\[(?!\[).*\]$/); +} + export function asStringValue(value: Value | undefined): StringValue | undefined { return value instanceof StringValue ? value : undefined; } @@ -59,6 +65,8 @@ export abstract class Value { public abstract getSpan(): language.Span; + // Note: This always includes the character after the Value as well (i.e., uses + // Contains.extended). public abstract contains(characterIndex: number): boolean; public abstract toString(): string; @@ -113,7 +121,7 @@ export class StringValue extends Value { } public contains(characterIndex: number): boolean { - return this.getSpan().contains(characterIndex, true); + return this.getSpan().contains(characterIndex, language.Contains.extended); } public hasCloseQuote(): boolean { @@ -175,7 +183,7 @@ export class NumberValue extends Value { } public contains(characterIndex: number): boolean { - return this.getSpan().contains(characterIndex, true); + return this.getSpan().contains(characterIndex, language.Contains.extended); } public accept(visitor: Visitor): void { @@ -253,7 +261,7 @@ export class ArrayAccessValue extends ParentValue { } public contains(characterIndex: number): boolean { - return this.getSpan().contains(characterIndex, !this._rightSquareBracketToken); + return this.getSpan().contains(characterIndex, this._rightSquareBracketToken ? language.Contains.strict : language.Contains.extended); } public accept(visitor: Visitor): void { @@ -434,7 +442,7 @@ export class FunctionCallValue extends ParentValue { } public contains(characterIndex: number): boolean { - return this.getSpan().contains(characterIndex, !this._rightParenthesisToken); + return this.getSpan().contains(characterIndex, this._rightParenthesisToken ? language.Contains.strict : language.Contains.extended); } public accept(visitor: Visitor): void { @@ -470,7 +478,7 @@ export class FunctionCallValue extends ParentValue { */ export class PropertyAccess extends ParentValue { // We need to allow creating a property access expresion whether the property name - // was correctly given or note, so we can have proper intellisense/etc. + // was correctly given or not, so we can have proper intellisense/etc. // I.e., we require the period, but after that might be empty or an error. constructor(private _source: Value, private _periodToken: Token, private _nameToken: Token | undefined) { super(); @@ -538,7 +546,7 @@ export class PropertyAccess extends ParentValue { } public contains(characterIndex: number): boolean { - return this.getSpan().contains(characterIndex, true); + return this.getSpan().contains(characterIndex, language.Contains.extended); } public accept(visitor: Visitor): void { @@ -558,7 +566,7 @@ export class PropertyAccess extends ParentValue { * A set of functions that pertain to getting highlight character indexes for a TLE string. */ export class BraceHighlighter { - public static getHighlightCharacterIndexes(context: PositionContext): number[] { + public static getHighlightCharacterIndexes(context: TemplatePositionContext): number[] { assert(context); let highlightCharacterIndexes: number[] = []; @@ -1113,7 +1121,7 @@ export class Tokenizer { * Convenient way of seeing what this object represents in the debugger, shouldn't be used for production code */ public get __debugDisplay(): string { - return __debugMarkSubstring(this._text, this._currentTokenStartIndex, this._current ? this._current.toString().length : 0); + return __debugMarkRangeInString(this._text, this._currentTokenStartIndex, this._current ? this._current.toString().length : 0); } public hasStarted(): boolean { diff --git a/src/TemplatePositionContext.ts b/src/TemplatePositionContext.ts new file mode 100644 index 000000000..4d9677cd8 --- /dev/null +++ b/src/TemplatePositionContext.ts @@ -0,0 +1,696 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:max-line-length + +import { AzureRMAssets, BuiltinFunctionMetadata } from "./AzureRMAssets"; +import { CachedValue } from "./CachedValue"; +import * as Completion from "./Completion"; +import { templateKeys } from "./constants"; +import { DeploymentTemplate } from "./DeploymentTemplate"; +import { assert } from './fixed_assert'; +import { IFunctionMetadata, IFunctionParameterMetadata } from "./IFunctionMetadata"; +import { INamedDefinition } from "./INamedDefinition"; +import { IParameterDefinition } from "./IParameterDefinition"; +import * as Json from "./JSON"; +import * as language from "./Language"; +import { DeploymentParameters } from "./parameterFiles/DeploymentParameters"; +import { IReferenceSite, PositionContext, ReferenceSiteKind } from "./PositionContext"; +import * as Reference from "./ReferenceList"; +import { TemplateScope } from "./TemplateScope"; +import * as TLE from "./TLE"; +import { UserFunctionDefinition } from "./UserFunctionDefinition"; +import { UserFunctionMetadata } from "./UserFunctionMetadata"; +import { UserFunctionNamespaceDefinition } from "./UserFunctionNamespaceDefinition"; +import { IVariableDefinition } from "./VariableDefinition"; + +/** + * Information about the TLE expression (if position is at an expression string) + */ +class TleInfo implements ITleInfo { + public constructor( + public readonly tleParseResult: TLE.ParseResult, + public readonly tleCharacterIndex: number, + public readonly tleValue: TLE.Value | undefined, + public readonly scope: TemplateScope + ) { + } +} + +/** + * Represents a position inside the snapshot of a deployment template, plus all related information + * that can be parsed and analyzed about it + */ +export class TemplatePositionContext extends PositionContext { + private _tleInfo: CachedValue = new CachedValue(); + + public static fromDocumentLineAndColumnIndexes(deploymentTemplate: DeploymentTemplate, documentLineIndex: number, documentColumnIndex: number, associatedParameters: DeploymentParameters | undefined): TemplatePositionContext { + let context = new TemplatePositionContext(deploymentTemplate, associatedParameters); + context.initFromDocumentLineAndColumnIndices(documentLineIndex, documentColumnIndex); + return context; + } + + public static fromDocumentCharacterIndex(deploymentTemplate: DeploymentTemplate, documentCharacterIndex: number, associatedParameters: DeploymentParameters | undefined): TemplatePositionContext { + let context = new TemplatePositionContext(deploymentTemplate, associatedParameters); + context.initFromDocumentCharacterIndex(documentCharacterIndex); + return context; + } + + private constructor(deploymentTemplate: DeploymentTemplate, associatedParameters: DeploymentParameters | undefined) { + super(deploymentTemplate, associatedParameters); + } + + public get document(): DeploymentTemplate { + return super.document; + } + + /** + * Retrieves TleInfo for the current position if it's inside a string + */ + public get tleInfo(): TleInfo | undefined { + return this._tleInfo.getOrCacheValue(() => { + //const tleParseResult = this._deploymentTemplate.getTLEParseResultFromJSONToken(this.jsonToken); + const jsonToken = this.jsonToken; + if ( + jsonToken + && jsonToken.type === Json.TokenType.QuotedString + && this.jsonValue + && this.jsonValue instanceof Json.StringValue + ) { + const tleParseResult = this.document.getTLEParseResultFromJsonStringValue(this.jsonValue); + const tleCharacterIndex = this.documentCharacterIndex - this.jsonTokenStartIndex; + const tleValue = tleParseResult.getValueAtCharacterIndex(tleCharacterIndex); + return new TleInfo(tleParseResult, tleCharacterIndex, tleValue, tleParseResult.scope); + } + return undefined; + }); + } + + /** + * If this position is inside an expression, inside a reference to an interesting function/parameter/etc, then + * return an object with information about this reference and the corresponding definition + */ + // tslint:disable-next-line:no-suspicious-comment + // CONSIDER: should includeDefinition should always be true? For instance, it would mean + // that we get hover over the definition of a param/var/etc and not just at references. + // Any bad side effects? + public getReferenceSiteInfo(includeDefinition: boolean): IReferenceSite | undefined { + const tleInfo = this.tleInfo; + if (tleInfo) { + const scope = tleInfo.scope; + const tleCharacterIndex = tleInfo.tleCharacterIndex; + const definitionDocument = this.document; + const referenceDocument = this.document; + + const tleFuncCall: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleInfo.tleValue); + if (tleFuncCall) { + if (tleFuncCall.namespaceToken && tleFuncCall.namespaceToken.span.contains(tleCharacterIndex, language.Contains.strict)) { + // Inside the namespace of a user-function reference + const ns = tleFuncCall.namespaceToken.stringValue; + const nsDefinition = scope.getFunctionNamespaceDefinition(ns); + if (nsDefinition) { + const referenceSpan: language.Span = tleFuncCall.namespaceToken.span.translate(this.jsonTokenStartIndex); + return { referenceKind: ReferenceSiteKind.reference, referenceDocument, definition: nsDefinition, referenceSpan, definitionDocument }; + } + } else if (tleFuncCall.nameToken) { + const referenceSpan: language.Span = tleFuncCall.nameToken.span.translate(this.jsonTokenStartIndex); + const referenceKind = ReferenceSiteKind.reference; + + if (tleFuncCall.nameToken.span.contains(tleCharacterIndex, language.Contains.strict)) { + if (tleFuncCall.namespaceToken) { + // Inside the name of a user-function reference + const ns = tleFuncCall.namespaceToken.stringValue; + const name = tleFuncCall.nameToken.stringValue; + const nsDefinition = scope.getFunctionNamespaceDefinition(ns); + const userFunctiondefinition = scope.getUserFunctionDefinition(ns, name); + if (nsDefinition && userFunctiondefinition) { + return { referenceKind, referenceDocument, definition: userFunctiondefinition, referenceSpan, definitionDocument }; + } + } else { + // Inside a reference to a built-in function + const functionMetadata: BuiltinFunctionMetadata | undefined = AzureRMAssets.getFunctionMetadataFromName(tleFuncCall.nameToken.stringValue); + if (functionMetadata) { + return { referenceKind, referenceDocument, definition: functionMetadata, referenceSpan, definitionDocument }; + } + } + } + } + } + + const tleStringValue: TLE.StringValue | undefined = TLE.asStringValue(tleInfo.tleValue); + if (tleStringValue instanceof TLE.StringValue) { + const referenceKind = ReferenceSiteKind.reference; + + if (tleStringValue.isParametersArgument()) { + // Inside the 'xxx' of a parameters('xxx') reference + const parameterDefinition: IParameterDefinition | undefined = scope.getParameterDefinition(tleStringValue.toString()); + if (parameterDefinition) { + const referenceSpan: language.Span = tleStringValue.getSpan().translate(this.jsonTokenStartIndex); + return { referenceKind, referenceDocument, definition: parameterDefinition, referenceSpan: referenceSpan, definitionDocument }; + } + } else if (tleStringValue.isVariablesArgument()) { + const variableDefinition: IVariableDefinition | undefined = scope.getVariableDefinition(tleStringValue.toString()); + if (variableDefinition) { + // Inside the 'xxx' of a variables('xxx') reference + const referenceSpan: language.Span = tleStringValue.getSpan().translate(this.jsonTokenStartIndex); + return { referenceKind, referenceDocument, definition: variableDefinition, referenceSpan: referenceSpan, definitionDocument }; + } + } + } + } + + if (includeDefinition) { + const definition = this.getDefinitionAtSite(); + if (definition && definition.nameValue) { + return { + referenceKind: ReferenceSiteKind.definition, + definition: definition, + referenceDocument: this.document, + definitionDocument: this.document, + referenceSpan: definition.nameValue?.unquotedSpan + }; + } + } + + return undefined; + } + + public getCompletionItems(): Completion.Item[] { + const tleInfo = this.tleInfo; + if (!tleInfo) { + // No string at this position + return []; + } + + // We're inside a JSON string. It may or may not contain square brackets. + + // The function/string/number/etc at the current position inside the string expression, + // or else the JSON string itself even it's not an expression + const tleValue: TLE.Value | undefined = tleInfo.tleValue; + const scope: TemplateScope = tleInfo.scope; + + if (!tleValue || !tleValue.contains(tleInfo.tleCharacterIndex)) { + // No TLE value here. For instance, expression is empty, or before/after/on the square brackets + if (TemplatePositionContext.isInsideSquareBrackets(tleInfo.tleParseResult, tleInfo.tleCharacterIndex)) { + // Inside brackets, so complete with all valid functions and namespaces + const replaceSpan = this.emptySpanAtDocumentCharacterIndex; + const functionCompletions = TemplatePositionContext.getMatchingFunctionCompletions(scope, undefined, "", replaceSpan); + const namespaceCompletions = TemplatePositionContext.getMatchingNamespaceCompletions(scope, "", replaceSpan); + return functionCompletions.concat(namespaceCompletions); + } else { + return []; + } + + } else if (tleValue instanceof TLE.FunctionCallValue) { + return this.getFunctionCallCompletions(tleValue, tleInfo.tleCharacterIndex, scope); + } else if (tleValue instanceof TLE.StringValue) { + return this.getStringLiteralCompletions(tleValue, tleInfo.tleCharacterIndex, scope); + } else if (tleValue instanceof TLE.PropertyAccess) { + return this.getPropertyAccessCompletions(tleValue, tleInfo.tleCharacterIndex, scope); + } + + return []; + } + + /** + * Given position in expression is past the left square bracket and before the right square bracket, + * *or* there is no square bracket yet + */ + private static isInsideSquareBrackets(parseResult: TLE.ParseResult, characterIndex: number): boolean { + const leftSquareBracketToken: TLE.Token | undefined = parseResult.leftSquareBracketToken; + const rightSquareBracketToken: TLE.Token | undefined = parseResult.rightSquareBracketToken; + + if (leftSquareBracketToken && leftSquareBracketToken.span.afterEndIndex <= characterIndex && + (!rightSquareBracketToken || characterIndex <= rightSquareBracketToken.span.startIndex)) { + return true; + } + + return false; + } + + /** + * Get completions when we're anywhere inside a string literal + */ + private getStringLiteralCompletions(tleValue: TLE.StringValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { + // Start at index 1 to skip past the opening single-quote. + const prefix: string = tleValue.toString().substring(1, tleCharacterIndex - tleValue.getSpan().startIndex); + + if (tleValue.isParametersArgument()) { + // The string is a parameter name inside a parameters('xxx') function + return this.getMatchingParameterCompletions(prefix, tleValue, tleCharacterIndex, scope); + } else if (tleValue.isVariablesArgument()) { + // The string is a variable name inside a variables('xxx') function + return this.getMatchingVariableCompletions(prefix, tleValue, tleCharacterIndex, scope); + } + + return []; + } + + /** + * Get completions when we're anywhere inside a property access, e.g. "resourceGroup().prop1.prop2" + */ + private getPropertyAccessCompletions(tleValue: TLE.PropertyAccess, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { + const functionSource: TLE.FunctionCallValue | undefined = tleValue.functionSource; + + // Property accesses always start with a function call (might be 'variables'/'parameters') + if (functionSource) { + let propertyPrefix: string = ""; + let replaceSpan: language.Span = this.emptySpanAtDocumentCharacterIndex; + const propertyNameToken: TLE.Token | undefined = tleValue.nameToken; + if (propertyNameToken) { + replaceSpan = propertyNameToken.span.translate(this.jsonTokenStartIndex); + propertyPrefix = propertyNameToken.stringValue.substring(0, tleCharacterIndex - propertyNameToken.span.startIndex).toLowerCase(); + } + + const variableProperty: IVariableDefinition | undefined = scope.getVariableDefinitionFromFunctionCall(functionSource); + const parameterProperty: IParameterDefinition | undefined = scope.getParameterDefinitionFromFunctionCall(functionSource); + const sourcesNameStack: string[] = tleValue.sourcesNameStack; + if (variableProperty) { + // [variables('xxx').prop] + + // Is the variable's value is an object? + const sourceVariableDefinition: Json.ObjectValue | undefined = Json.asObjectValue(variableProperty.value); + if (sourceVariableDefinition) { + return this.getDeepPropertyAccessCompletions( + propertyPrefix, + sourceVariableDefinition, + sourcesNameStack, + replaceSpan); + } + } else if (parameterProperty) { + // [parameters('xxx').prop] + + // Is the parameters's default valuean object? + const parameterDefValue: Json.ObjectValue | undefined = parameterProperty.defaultValue ? Json.asObjectValue(parameterProperty.defaultValue) : undefined; + if (parameterDefValue) { + const sourcePropertyDefinition: Json.ObjectValue | undefined = Json.asObjectValue(parameterDefValue.getPropertyValueFromStack(sourcesNameStack)); + if (sourcePropertyDefinition) { + return this.getDeepPropertyAccessCompletions( + propertyPrefix, + sourcePropertyDefinition, + sourcesNameStack, + replaceSpan); + } + } + } else if (sourcesNameStack.length === 0) { + // [function(...).prop] + + // We don't allow multiple levels of property access + // (resourceGroup().prop1.prop2) on functions other than variables/parameters, + // therefore checking that sourcesNameStack.length === 0 + const functionName: string | undefined = functionSource.name; + + // Don't currently support completions from a user function returning an object, + // so there must be no function namespace + if (functionName && !functionSource.namespaceToken) { + let functionMetadata: BuiltinFunctionMetadata | undefined = AzureRMAssets.getFunctionMetadataFromName(functionName); + if (functionMetadata) { + // Property completion off of a built-in function. Completions will consist of the + // returnValueMembers of the function, if any. + const result: Completion.Item[] = []; + for (const returnValueMember of functionMetadata.returnValueMembers) { + if (propertyPrefix === "" || returnValueMember.toLowerCase().startsWith(propertyPrefix)) { + result.push(TemplatePositionContext.createPropertyCompletionItem(returnValueMember, replaceSpan)); + } + } + + return result; + } + } + } + } + + return []; + } + + /** + * Return completions when we're anywhere inside a function call expression + */ + // tslint:disable-next-line: max-func-body-length cyclomatic-complexity // Pretty straightforward, don't think further refactoring is important + private getFunctionCallCompletions(tleValue: TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { + assert(tleValue.getSpan().contains(tleCharacterIndex, language.Contains.extended), "Position should be inside the function call, or right after it"); + + const namespaceName: string | undefined = tleValue.namespaceToken ? tleValue.namespaceToken.stringValue : undefined; + // tslint:disable-next-line: strict-boolean-expressions + const namespace: UserFunctionNamespaceDefinition | undefined = (namespaceName && scope.getFunctionNamespaceDefinition(namespaceName)) || undefined; + + // The token (namespace or name) that the user is completing and will be replaced with the user's selection + // If undefined, we're just inserting at the current position, not replacing anything + let tleTokenToComplete: TLE.Token | undefined; + + let completeNamespaces: boolean; + let completeBuiltinFunctions: boolean; + let completeUserFunctions: boolean; + + if (tleValue.nameToken && tleValue.nameToken.span.contains(tleCharacterIndex, language.Contains.extended)) { + // The caret is inside the function's name (or a namespace before the period has been typed), so one of + // three possibilities. + tleTokenToComplete = tleValue.nameToken; + + if (namespace) { + // 1) "namespace.function" + // Complete only UDF functions + completeUserFunctions = true; + completeNamespaces = false; + completeBuiltinFunctions = false; + } else { + // 2) "namespace" + // 3) "function" + // Complete built-ins and namespaces + completeNamespaces = true; + completeBuiltinFunctions = true; + completeUserFunctions = false; + } + } else if (namespaceName && tleValue.periodToken && tleValue.periodToken.span.afterEndIndex === tleCharacterIndex) { + // "namespace.function" + // The caret is right after the period between a namespace and a function name, so we will be looking for UDF function completions + + if (!namespace) { + // The given namespace is not defined, so no completions + return []; + } + + tleTokenToComplete = tleValue.nameToken; + completeNamespaces = false; + completeBuiltinFunctions = false; + completeUserFunctions = true; + } else if (tleValue.namespaceToken && tleValue.periodToken && tleValue.namespaceToken.span.contains(tleCharacterIndex, language.Contains.extended)) { + // "namespace.function" + // The caret is inside the UDF's namespace (e.g., the namespace and at least a period already exist in the call). + // + // So we want built-in functions or namespaces only + + tleTokenToComplete = tleValue.namespaceToken; + completeNamespaces = true; + completeBuiltinFunctions = true; + completeUserFunctions = false; + + } else if (tleValue.isCallToBuiltinWithName(templateKeys.parameters) && tleValue.argumentExpressions.length === 0) { + // "parameters" or "parameters()" or similar + return this.getMatchingParameterCompletions("", tleValue, tleCharacterIndex, scope); + } else if (tleValue.isCallToBuiltinWithName(templateKeys.variables) && tleValue.argumentExpressions.length === 0) { + // "variables" or "variables()" or similar + return this.getMatchingVariableCompletions("", tleValue, tleCharacterIndex, scope); + } else { + // Anywhere else (e.g. whitespace after function name, or inside the arguments list). + // + // "function ()" + // "function()" + // etc. + // + // Assume the user is starting a new function call and provide all completions at that location; + + tleTokenToComplete = undefined; + completeNamespaces = true; + completeBuiltinFunctions = true; + completeUserFunctions = false; + } + + let replaceSpan: language.Span; + let completionPrefix: string; + + // Figure out the span which will be replaced by the completion + if (tleTokenToComplete) { + const tokenToCompleteStartIndex: number = tleTokenToComplete.span.startIndex; + completionPrefix = tleTokenToComplete.stringValue.substring(0, tleCharacterIndex - tokenToCompleteStartIndex); + if (completionPrefix.length === 0) { + replaceSpan = this.emptySpanAtDocumentCharacterIndex; + } else { + replaceSpan = tleTokenToComplete.span.translate(this.jsonTokenStartIndex); + } + } else { + // Nothing getting completed, completion selection will be inserted at current location + replaceSpan = this.emptySpanAtDocumentCharacterIndex; + completionPrefix = ""; + } + + assert(completeBuiltinFunctions || completeUserFunctions || completeNamespaces, "Should be completing something"); + let builtinCompletions: Completion.Item[] = []; + let userFunctionCompletions: Completion.Item[] = []; + let namespaceCompletions: Completion.Item[] = []; + + if (completeBuiltinFunctions || completeUserFunctions) { + if (completeUserFunctions && namespace) { + userFunctionCompletions = TemplatePositionContext.getMatchingFunctionCompletions(scope, namespace, completionPrefix, replaceSpan); + } + if (completeBuiltinFunctions) { + builtinCompletions = TemplatePositionContext.getMatchingFunctionCompletions(scope, undefined, completionPrefix, replaceSpan); + } + } + if (completeNamespaces) { + namespaceCompletions = TemplatePositionContext.getMatchingNamespaceCompletions(scope, completionPrefix, replaceSpan); + } + + return builtinCompletions.concat(namespaceCompletions).concat(userFunctionCompletions); + } + + private getDeepPropertyAccessCompletions(propertyPrefix: string, variableOrParameterDefinition: Json.ObjectValue, sourcesNameStack: string[], replaceSpan: language.Span): Completion.Item[] { + const result: Completion.Item[] = []; + + const sourcePropertyDefinitionObject: Json.ObjectValue | undefined = Json.asObjectValue(variableOrParameterDefinition.getPropertyValueFromStack(sourcesNameStack)); + if (sourcePropertyDefinitionObject) { + let matchingPropertyNames: string[]; + if (!propertyPrefix) { + matchingPropertyNames = sourcePropertyDefinitionObject.propertyNames; + } else { + // We need to ignore casing when creating completions + const propertyPrefixLC = propertyPrefix.toLowerCase(); + + matchingPropertyNames = []; + for (const propertyName of sourcePropertyDefinitionObject.propertyNames) { + if (propertyName.toLowerCase().startsWith(propertyPrefixLC)) { + matchingPropertyNames.push(propertyName); + } + } + } + + for (const matchingPropertyName of matchingPropertyNames) { + result.push(TemplatePositionContext.createPropertyCompletionItem(matchingPropertyName, replaceSpan)); + } + } + + return result; + } + + private static createPropertyCompletionItem(propertyName: string, replaceSpan: language.Span): Completion.Item { + return Completion.Item.fromPropertyName(propertyName, replaceSpan); + } + + /** + * Return all references to the given reference site info in this document + * @returns undefined if references are not supported at this location, or empty list if supported but none found + */ + protected getReferencesCore(): Reference.ReferenceList | undefined { + const tleInfo = this.tleInfo; + if (tleInfo) { // If we're inside a string (whether an expression or not) + const refInfo = this.getReferenceSiteInfo(true); + if (refInfo) { + return this.document.findReferencesToDefinition(refInfo.definition); + } + } + + return undefined; + } + + /** + * Returns the definition at the current position, if the current position represents + * a definition. + */ + private getDefinitionAtSite(): INamedDefinition | undefined { + const tleInfo = this.tleInfo; + if (tleInfo) { + const jsonStringValue: Json.StringValue | undefined = Json.asStringValue(this.jsonValue); + if (jsonStringValue) { + const unquotedString = jsonStringValue.unquotedValue; + const scope = tleInfo.scope; + + // Is it a parameter definition? + const parameterDefinition: IParameterDefinition | undefined = scope.getParameterDefinition(unquotedString); + if (parameterDefinition && parameterDefinition.nameValue === jsonStringValue) { + return parameterDefinition; + } + + // Is it a variable definition? + const variableDefinition: IVariableDefinition | undefined = scope.getVariableDefinition(unquotedString); + if (variableDefinition && variableDefinition.nameValue === jsonStringValue) { + return variableDefinition; + } + + // Is it a user namespace definition? + const namespaceDefinition: UserFunctionNamespaceDefinition | undefined = scope.getFunctionNamespaceDefinition(unquotedString); + if (namespaceDefinition && namespaceDefinition.nameValue === jsonStringValue) { + return namespaceDefinition; + } + + // Is it a user function definition inside any namespace? + for (let ns of scope.namespaceDefinitions) { + const userFunctionDefinition: UserFunctionDefinition | undefined = scope.getUserFunctionDefinition(ns.nameValue.unquotedValue, unquotedString); + if (userFunctionDefinition && userFunctionDefinition.nameValue === jsonStringValue) { + return userFunctionDefinition; + } + } + } + } + } + + public getSignatureHelp(): TLE.FunctionSignatureHelp | undefined { + const tleValue: TLE.Value | undefined = this.tleInfo && this.tleInfo.tleValue; + if (this.tleInfo && tleValue) { + let functionToHelpWith: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleValue); + if (!functionToHelpWith) { + functionToHelpWith = TLE.asFunctionCallValue(tleValue.parent); + } + + if (functionToHelpWith && functionToHelpWith.name) { + let functionMetadata: IFunctionMetadata | undefined; + + if (functionToHelpWith.namespaceToken) { + // Call to user-defined function + const namespace: string = functionToHelpWith.namespaceToken.stringValue; + const name: string | undefined = functionToHelpWith.name; + const udfDefinition: UserFunctionDefinition | undefined = this.tleInfo.scope.getUserFunctionDefinition(namespace, name); + functionMetadata = udfDefinition ? UserFunctionMetadata.fromDefinition(udfDefinition) : undefined; + } else { + // Call to built-in function + functionMetadata = AzureRMAssets.getFunctionMetadataFromName(functionToHelpWith.name); + } + if (functionMetadata) { + let currentArgumentIndex: number = 0; + + for (const commaToken of functionToHelpWith.commaTokens) { + if (commaToken.span.startIndex < this.tleInfo.tleCharacterIndex) { + ++currentArgumentIndex; + } + } + + const functionMetadataParameters: IFunctionParameterMetadata[] = functionMetadata.parameters; + if (functionMetadataParameters.length > 0 && + functionMetadataParameters.length <= currentArgumentIndex && + functionMetadataParameters[functionMetadataParameters.length - 1].name.endsWith("...")) { + + currentArgumentIndex = functionMetadataParameters.length - 1; + } + + return new TLE.FunctionSignatureHelp(currentArgumentIndex, functionMetadata); + } + } + } + + return undefined; + } + + /** + * Given a possible namespace name plus a function name prefix and replacement span, return a list + * of completions for functions or namespaces starting with that prefix + */ + private static getMatchingFunctionCompletions(scope: TemplateScope, namespace: UserFunctionNamespaceDefinition | undefined, functionNamePrefix: string, replaceSpan: language.Span): Completion.Item[] { + let matches: IFunctionMetadata[]; + + if (namespace) { + // User-defined function + matches = scope.findFunctionDefinitionsWithPrefix(namespace, functionNamePrefix).map(fd => UserFunctionMetadata.fromDefinition(fd)); + } else { + // Built-in function + matches = functionNamePrefix === "" ? + AzureRMAssets.getFunctionsMetadata().functionMetadata : + AzureRMAssets.getFunctionMetadataFromPrefix(functionNamePrefix); + } + + return matches.map(m => Completion.Item.fromFunctionMetadata(m, replaceSpan)); + } + + /** + * Given a possible namespace name plus a function name prefix and replacement span, return a list + * of completions for functions or namespaces starting with that prefix + */ + private static getMatchingNamespaceCompletions(scope: TemplateScope, namespacePrefix: string, replaceSpan: language.Span): Completion.Item[] { + const matches: UserFunctionNamespaceDefinition[] = scope.findNamespaceDefinitionsWithPrefix(namespacePrefix); + return matches.map(m => Completion.Item.fromNamespaceDefinition(m, replaceSpan)); + } + + private getMatchingParameterCompletions(prefix: string, tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { + const replaceSpanInfo: ReplaceSpanInfo = this.getReplaceSpanInfo(tleValue, tleCharacterIndex); + + const parameterCompletions: Completion.Item[] = []; + const parameterDefinitionMatches: IParameterDefinition[] = scope.findParameterDefinitionsWithPrefix(prefix); + for (const parameterDefinition of parameterDefinitionMatches) { + parameterCompletions.push(Completion.Item.fromParameterDefinition(parameterDefinition, replaceSpanInfo.replaceSpan, replaceSpanInfo.includeRightParenthesisInCompletion)); + } + return parameterCompletions; + } + + private getMatchingVariableCompletions(prefix: string, tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number, scope: TemplateScope): Completion.Item[] { + const replaceSpanInfo: ReplaceSpanInfo = this.getReplaceSpanInfo(tleValue, tleCharacterIndex); + + const variableCompletions: Completion.Item[] = []; + const variableDefinitionMatches: IVariableDefinition[] = scope.findVariableDefinitionsWithPrefix(prefix); + for (const variableDefinition of variableDefinitionMatches) { + variableCompletions.push(Completion.Item.fromVariableDefinition(variableDefinition, replaceSpanInfo.replaceSpan, replaceSpanInfo.includeRightParenthesisInCompletion)); + } + return variableCompletions; + } + + private getReplaceSpanInfo(tleValue: TLE.StringValue | TLE.FunctionCallValue, tleCharacterIndex: number): ReplaceSpanInfo { + let includeRightParenthesisInCompletion: boolean = true; + let replaceSpan: language.Span; + if (tleValue instanceof TLE.StringValue) { + const stringSpan: language.Span = tleValue.getSpan(); + const stringStartIndex: number = stringSpan.startIndex; + const functionValue: TLE.FunctionCallValue | undefined = TLE.asFunctionCallValue(tleValue.parent); + + const rightParenthesisIndex: number = tleValue.toString().indexOf(")"); + const rightSquareBracketIndex: number = tleValue.toString().indexOf("]"); + if (rightParenthesisIndex >= 0) { + replaceSpan = new language.Span(stringStartIndex, rightParenthesisIndex + 1); + } else if (rightSquareBracketIndex >= 0) { + replaceSpan = new language.Span(stringStartIndex, rightSquareBracketIndex); + } else if (functionValue && functionValue.rightParenthesisToken && functionValue.argumentExpressions.length === 1) { + replaceSpan = new language.Span(stringStartIndex, functionValue.rightParenthesisToken.span.afterEndIndex - stringStartIndex); + } else { + includeRightParenthesisInCompletion = !!functionValue && functionValue.argumentExpressions.length <= 1; + replaceSpan = stringSpan; + } + + replaceSpan = replaceSpan.translate(this.jsonTokenStartIndex); + } else { + if (tleValue.rightParenthesisToken) { + replaceSpan = new language.Span( + this.documentCharacterIndex, + tleValue.rightParenthesisToken.span.startIndex - tleCharacterIndex + 1); + } else { + replaceSpan = this.emptySpanAtDocumentCharacterIndex; + } + } + + return { + includeRightParenthesisInCompletion: includeRightParenthesisInCompletion, + replaceSpan: replaceSpan + }; + } +} + +interface ReplaceSpanInfo { + includeRightParenthesisInCompletion: boolean; + replaceSpan: language.Span; +} + +interface ITleInfo { + /** + * The parse result of the enclosing string, if we're inside a string (it's with an expression or not) + */ + tleParseResult: TLE.ParseResult; + + /** + * The index inside the enclosing string, if we're inside a string (whether it's an expression or not) + */ + tleCharacterIndex: number; + + /** + * The outermost TLE value enclosing the current position, if we're inside a string + * (whether it's an expression or not). This can undefined if inside square brackets but before + * an expression, etc. + */ + tleValue: TLE.Value | undefined; + +} diff --git a/src/Tokenizer.ts b/src/Tokenizer.ts index 3ae6a4621..d85e7264c 100644 --- a/src/Tokenizer.ts +++ b/src/Tokenizer.ts @@ -6,6 +6,9 @@ // tslint:disable:cyclomatic-complexity // Grandfathered in // tslint:disable:variable-name +// tslint:disable-next-line:no-suspicious-comment +// TODO: rename this module to refer to "basic tokens", etc. + import * as utilities from "./Utilities"; /** diff --git a/src/Treeview.ts b/src/Treeview.ts index 7148dc71b..31903fe32 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -10,9 +10,12 @@ import * as path from 'path'; import * as vscode from "vscode"; -import { iconsPath, languageId, templateKeys } from "./constants"; +import { armTemplateLanguageId, iconsPath, templateKeys } from "./constants"; import { assert } from './fixed_assert'; import * as Json from "./JSON"; +import * as language from "./Language"; + +const Contains = language.Contains; const topLevelIcons: [string, string][] = [ ["$schema", "label.svg"], @@ -215,7 +218,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { } } else { let elementInfo = JSON.parse(element); - let valueNode = elementInfo.current.value.start !== undefined ? this.tree.getValueAtCharacterIndex(elementInfo.current.value.start) : undefined; + let valueNode = elementInfo.current.value.start !== undefined ? this.tree.getValueAtCharacterIndex(elementInfo.current.value.start, Contains.strict) : undefined; // Value is an object and is collapsible if (valueNode instanceof Json.ObjectValue && elementInfo.current.collapsible) { @@ -295,12 +298,12 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { */ private getContextValue(elementInfo: IElementInfo): string | undefined { if (elementInfo.current.level === 1) { - const keyNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start); + const keyNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start, Contains.strict); if (keyNode instanceof Json.StringValue) { return keyNode.unquotedValue; } } else { - const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start); + const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start, Contains.strict); if (rootNode instanceof Json.StringValue) { return rootNode.unquotedValue; } @@ -309,7 +312,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { } private getTreeNodeLabel(elementInfo: IElementInfo): string { - const keyNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start); + const keyNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start, Contains.strict); // Key is an object (e.g. a resource object) if (keyNode instanceof Json.ObjectValue) { @@ -350,7 +353,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { return toFriendlyString(keyNode); } else if (elementInfo.current.value.start !== undefined) { // For other value types, display key and value since they won't be expandable - const valueNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.value.start); + const valueNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.value.start, Contains.strict); return `${keyNode instanceof Json.StringValue ? toFriendlyString(keyNode) : "?"}: ${toFriendlyString(valueNode)}`; } @@ -471,7 +474,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { private getIconPath(elementInfo: IElementInfo): string | undefined { let icon: string | undefined; - const keyOrResourceNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start); + const keyOrResourceNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.current.key.start, Contains.strict); // Is current element a root element? if (elementInfo.current.level === 1) { @@ -482,7 +485,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { // Is current element an element of a root element? // Get root value - const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start); + const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start, Contains.strict); if (rootNode) { icon = this.getIcon(topLevelChildIconsByRootNode, rootNode.toString(), ""); } @@ -491,7 +494,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { // If resourceType element is found on resource objects set to specific resourceType Icon or else a default resource icon // tslint:disable-next-line: strict-boolean-expressions if (elementInfo.current.level && elementInfo.current.level > 1) { - const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start); + const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start, Contains.strict); if (elementInfo.current.key.kind === Json.ValueKind.ObjectValue && rootNode && rootNode.toString().toUpperCase() === "resources".toUpperCase() && keyOrResourceNode instanceof Json.ObjectValue) { @@ -555,7 +558,7 @@ export class JsonOutlineProvider implements vscode.TreeDataProvider { private shouldShowTreeForDocument(document: vscode.TextDocument): boolean { // Only show view if the language is set to Azure Resource Manager Template - return document.languageId === languageId; + return document.languageId === armTemplateLanguageId; } private setTreeViewContext(visible: boolean): void { diff --git a/src/UserFunctionNamespaceDefinition.ts b/src/UserFunctionNamespaceDefinition.ts index cc372cd41..6de4c295b 100644 --- a/src/UserFunctionNamespaceDefinition.ts +++ b/src/UserFunctionNamespaceDefinition.ts @@ -23,7 +23,7 @@ export class UserFunctionNamespaceDefinition implements INamedDefinition { /* Example: "functions": [ - { <<<< call createIfValid on this object + { <---- call createIfValid on this object "namespace": "contoso", "members": { "uniqueName": { diff --git a/src/VariableDefinition.ts b/src/VariableDefinition.ts index 7a5a8b56f..832527b04 100644 --- a/src/VariableDefinition.ts +++ b/src/VariableDefinition.ts @@ -197,7 +197,7 @@ export class TopLevelCopyBlockVariableDefinition extends VariableDefinition { // E.g. // "variables": { // "copy": [ - // { <<<< This is passed to constructor + // { <---- This is passed to constructor // "name": "top-level-string-array", // "count": 5, // "input": "[concat('myDataDisk', copyIndex('top-level-string-array', 1))]" diff --git a/src/constants.ts b/src/constants.ts index d743c8863..ca46d69b6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,7 +18,7 @@ export const iconsPath = path.join(basePath, "icons"); export const languageServerName = 'ARM Template Language Server'; export const languageFriendlyName = 'Azure Resource Manager Template'; -export const languageId = 'arm-template'; +export const armTemplateLanguageId = 'arm-template'; export const languageServerFolderName = 'languageServer'; export const extensionName = 'Azure Resource Manager Tools'; export const outputWindowName = extensionName; diff --git a/src/debugMarkStrings.ts b/src/debugMarkStrings.ts index 7ece0f4f1..e2b4933c4 100644 --- a/src/debugMarkStrings.ts +++ b/src/debugMarkStrings.ts @@ -7,7 +7,17 @@ * and truncates the beginning and end of a long string. */ // tslint:disable-next-line:function-name -export function __debugMarkPositionInString(text: string, position: number, insertTextAtPosition: string, charactersBeforeIndex: number = 25, charactersAfterPosition: number = 50): string { +export function __debugMarkPositionInString( + text: string, + position: number, + insertTextAtPosition: string = '', + charactersBeforeIndex: number = 45, + charactersAfterPosition: number = 50 +): string { + if (position >= text.length) { + const textAtEnd = `${text.slice(text.length - charactersAfterPosition)}`; + return `${textAtEnd}...`; + } const preTextIndex = position - charactersBeforeIndex; const preText = `${(preTextIndex > 0 ? "..." : "")}${text.slice(preTextIndex >= 0 ? preTextIndex : 0, position)}`; @@ -17,8 +27,22 @@ export function __debugMarkPositionInString(text: string, position: number, inse return `${preText}${insertTextAtPosition}${postTextIndex}`; } +/** + * Same as __debugMarkPositionInString, but specifying a range instead of a position + */ // tslint:disable-next-line:function-name -export function __debugMarkSubstring(text: string, position: number, length: number, leftMarker: string = "<<", rightMarker: string = ">>", charactersBeforeIndex: number = 25, charactersAfterPosition: number = 50): string { +export function __debugMarkRangeInString( + text: string, + position: number, + length: number, + leftMarker: string = "<<", + rightMarker: string = ">>", + charactersBeforeIndex: number = 25, + charactersAfterPosition: number = 50 +): string { + if (position >= text.length) { + return __debugMarkPositionInString(text, position, leftMarker + rightMarker, charactersBeforeIndex, charactersAfterPosition); + } const preTextIndex = position - charactersBeforeIndex; const preText = `${(preTextIndex > 0 ? "..." : "")}${text.slice(preTextIndex >= 0 ? preTextIndex : 0, position)}`; diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index e8e5cb0b1..18450658c 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -7,29 +7,13 @@ import * as os from 'os'; import * as vscode from "vscode"; import { IAzExtOutputChannel, IAzureUserInput, ITelemetryReporter } from "vscode-azureextensionui"; import { LanguageClient } from "vscode-languageclient"; -import { isWebpack } from "./constants"; -import { assert } from "./fixed_assert"; +import { CompletionsSpy } from "./CompletionsSpy"; +import { IConfiguration, VsCodeConfiguration } from "./Configuration"; +import { configPrefix, isWebpack } from "./constants"; import { LanguageServerState } from "./languageclient/startArmLanguageServer"; +import { DeploymentFileMapping } from "./parameterFiles/DeploymentFileMapping"; import { JsonOutlineProvider } from "./Treeview"; - -/** - * Represents a scalar value that must be initialized before its getValue is called - */ -class InitializeBeforeUse { - private _value: { value: T; initialized: true } | { initialized: false } = { initialized: false }; - - public setValue(value: T): void { - this._value = { value: value, initialized: true }; - } - - public getValue(): T { - if (this._value.initialized) { - return this._value.value; - } else { - assert.fail("ExtensionVariables has not been fully initialized"); - } - } -} +import { InitializeBeforeUse } from "./util/InitializeBeforeUse"; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -88,6 +72,11 @@ class ExtensionVariables { // Suite support - lets us know when diagnostics have been completely published for a file public addCompletedDiagnostic: boolean = false; + + public readonly configuration: IConfiguration = new VsCodeConfiguration(configPrefix); + + public readonly completionItemsSpy: CompletionsSpy = new CompletionsSpy(); + public deploymentFileMapping: InitializeBeforeUse = new InitializeBeforeUse(); } // tslint:disable-next-line: no-any diff --git a/src/languageclient/startArmLanguageServer.ts b/src/languageclient/startArmLanguageServer.ts index b84905133..fd4a63b5c 100644 --- a/src/languageclient/startArmLanguageServer.ts +++ b/src/languageclient/startArmLanguageServer.ts @@ -10,10 +10,10 @@ import { ProgressLocation, window, workspace } from 'vscode'; import { callWithTelemetryAndErrorHandling, callWithTelemetryAndErrorHandlingSync, IActionContext, parseError } from 'vscode-azureextensionui'; import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions } from 'vscode-languageclient'; import { dotnetAcquire, ensureDotnetDependencies } from '../acquisition/dotnetAcquisition'; -import { configKeys, configPrefix, dotnetVersion, languageFriendlyName, languageId, languageServerFolderName, languageServerName } from '../constants'; +import { armTemplateLanguageId, configKeys, configPrefix, dotnetVersion, languageFriendlyName, languageServerFolderName, languageServerName } from '../constants'; import { ext } from '../extensionVariables'; import { assert } from '../fixed_assert'; -import { armDeploymentDocumentSelector } from '../supported'; +import { templateDocumentSelector } from '../supported'; import { WrappedErrorHandler } from './WrappedErrorHandler'; const languageServerDllName = 'Microsoft.ArmLanguageServer.dll'; @@ -115,7 +115,7 @@ export async function startLanguageClient(serverDllPath: string, dotnetExePath: // Options to control the language client let clientOptions: LanguageClientOptions = { - documentSelector: armDeploymentDocumentSelector, + documentSelector: templateDocumentSelector, diagnosticCollectionName: `${languageServerName} diagnostics`, outputChannel: ext.outputChannel, // Use the same output channel as the extension does revealOutputChannelOn: RevealOutputChannelOn.Error, @@ -133,7 +133,7 @@ export async function startLanguageClient(serverDllPath: string, dotnetExePath: ext.outputChannel.appendLine(`Client options:${os.EOL}${JSON.stringify(clientOptions, undefined, 2)}`); ext.outputChannel.appendLine(`Server options:${os.EOL}${JSON.stringify(serverOptions, undefined, 2)}`); let client: LanguageClient = new LanguageClient( - languageId, + armTemplateLanguageId, languageFriendlyName, // Used in the Output window combobox serverOptions, clientOptions diff --git a/src/editParameterFile.ts b/src/parameterFileGeneration.ts similarity index 69% rename from src/editParameterFile.ts rename to src/parameterFileGeneration.ts index 132ed9eee..930ec80ce 100644 --- a/src/editParameterFile.ts +++ b/src/parameterFileGeneration.ts @@ -6,6 +6,7 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import { QuickPickItem, Uri, window } from "vscode"; import { IActionContext, UserCancelledError } from 'vscode-azureextensionui'; +import { Json, TLE } from '../extension.bundle'; import { CaseInsensitiveMap } from './CaseInsensitiveMap'; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ExpressionType } from './ExpressionType'; @@ -14,9 +15,9 @@ import { IParameterDefinition } from './IParameterDefinition'; import { assertNever } from './util/assertNever'; import { indentMultilineString, unindentMultilineString } from './util/multilineStrings'; -const defaultIndent: number = 4; +export const defaultTabSize: number = 4; -export async function queryCreateParameterFile(actionContext: IActionContext, templateUri: Uri, template: DeploymentTemplate, indent: number = defaultIndent): Promise { +export async function queryCreateParameterFile(actionContext: IActionContext, templateUri: Uri, template: DeploymentTemplate, tabSize: number = defaultTabSize): Promise { const all = { label: "All parameters" }; const required = { label: "Only required parameters", description: "Uses only parameters that have no default value in the template file" }; @@ -42,7 +43,7 @@ export async function queryCreateParameterFile(actionContext: IActionContext, te throw new UserCancelledError(); } - let paramsObj: string = createParameterFileContents(template, indent, onlyRequiredParams); + let paramsObj: string = createParameterFileContents(template, tabSize, onlyRequiredParams); await fse.writeFile(newUri.fsPath, paramsObj, { encoding: 'utf8' }); @@ -50,7 +51,7 @@ export async function queryCreateParameterFile(actionContext: IActionContext, te return newUri; } -export function createParameterFileContents(template: DeploymentTemplate, indent: number, onlyRequiredParameters: boolean): string { +export function createParameterFileContents(template: DeploymentTemplate, tabSize: number, onlyRequiredParameters: boolean): string { /* e.g. { @@ -65,9 +66,9 @@ export function createParameterFileContents(template: DeploymentTemplate, indent */ - const tab = makeIndent(indent); + const tab = makeIndent(tabSize); - const params: CaseInsensitiveMap = createParameters(template, indent, onlyRequiredParameters); + const params: CaseInsensitiveMap = createParameters(template, tabSize, onlyRequiredParameters); const paramsContent = params.map((key, value) => value).join(`,${ext.EOL}`); // tslint:disable-next-line: prefer-template @@ -77,7 +78,7 @@ export function createParameterFileContents(template: DeploymentTemplate, indent `${tab}"parameters": {` + ext.EOL; if (params.size > 0) { - contents += indentMultilineString(paramsContent, indent * 2) + ext.EOL; + contents += indentMultilineString(paramsContent, tabSize * 2) + ext.EOL; } // tslint:disable-next-line: prefer-template @@ -87,7 +88,11 @@ export function createParameterFileContents(template: DeploymentTemplate, indent return contents; } -export function createParameterProperty(template: DeploymentTemplate, parameter: IParameterDefinition, indent: number): string { +/** + * Creates text for a property using information for that property in a template file + * @param tabSize The number of spaces to indent at each level. The parameter text will start flush left + */ +export function createParameterFromTemplateParameter(template: DeploymentTemplate, parameter: IParameterDefinition, tabSize: number = defaultTabSize): string { /* e.g. "parameters": { @@ -98,18 +103,27 @@ export function createParameterProperty(template: DeploymentTemplate, parameter: */ - let value: string = getDefaultValueFromType(parameter.validType, indent); + let value: string | undefined; if (parameter.defaultValue) { - const defValueSpan = parameter.defaultValue.span; - const defValue: string = template.documentText.slice(defValueSpan.startIndex, defValueSpan.afterEndIndex); - value = unindentMultilineString(defValue, true); + // If the parameter has a default value that's not an expression, then use it as the + // value in the param file + const isExpression = parameter.defaultValue instanceof Json.StringValue && + TLE.isTleExpression(parameter.defaultValue.unquotedValue); + if (!isExpression) { + const defValueSpan = parameter.defaultValue.span; + const defValue: string = template.documentText.slice(defValueSpan.startIndex, defValueSpan.afterEndIndex); + value = unindentMultilineString(defValue, true); + } + } + if (value === undefined) { + value = getDefaultValueFromType(parameter.validType, tabSize); } - const valueIndentedAfterFirstLine: string = indentMultilineString(value.trimLeft(), indent).trimLeft(); + const valueIndentedAfterFirstLine: string = indentMultilineString(value.trimLeft(), tabSize).trimLeft(); // tslint:disable-next-line:prefer-template return `"${parameter.nameValue.unquotedValue}": {` + ext.EOL - + `${makeIndent(indent)}"value": ${valueIndentedAfterFirstLine}` + ext.EOL + + `${makeIndent(tabSize)}"value": ${valueIndentedAfterFirstLine}` + ext.EOL + `}`; } @@ -141,18 +155,18 @@ function getDefaultValueFromType(propType: ExpressionType | undefined, indent: n } } -function createParameters(template: DeploymentTemplate, indent: number, onlyRequiredParameters: boolean): CaseInsensitiveMap { +function createParameters(template: DeploymentTemplate, tabSize: number, onlyRequiredParameters: boolean): CaseInsensitiveMap { let params: CaseInsensitiveMap = new CaseInsensitiveMap(); for (let paramDef of template.topLevelScope.parameterDefinitions) { if (!onlyRequiredParameters || !paramDef.defaultValue) { - params.set(paramDef.nameValue.unquotedValue, createParameterProperty(template, paramDef, indent)); + params.set(paramDef.nameValue.unquotedValue, createParameterFromTemplateParameter(template, paramDef, tabSize)); } } return params; } -function makeIndent(indent: number): string { - return ' '.repeat(indent); +function makeIndent(tabSize: number): string { + return ' '.repeat(tabSize); } diff --git a/src/parameterFiles/DeploymentFileMapping.ts b/src/parameterFiles/DeploymentFileMapping.ts new file mode 100644 index 000000000..e8e1ea797 --- /dev/null +++ b/src/parameterFiles/DeploymentFileMapping.ts @@ -0,0 +1,142 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import * as path from 'path'; +import { isNullOrUndefined } from 'util'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IConfiguration } from '../Configuration'; +import { configKeys } from '../constants'; +import { normalizePath } from '../util/normalizePath'; +import { getRelativeParameterFilePath, resolveParameterFilePath } from './parameterFiles'; + +interface IMapping { + // Path using same casing as specified (but /.. and double slashes normalized). + // Therefore appropriate to show to the user + resolvedTemplate: Uri; + // Completely normalized, resolved path (i.e. lower-cased on Win32). + // Therefore appropriate for use as a key + normalizedTemplate: Uri; + + resolvedParams: Uri; + normalizedParams: Uri; +} + +export class DeploymentFileMapping { + private _mapToParams: Map | undefined; + private _mapToTemplates: Map | undefined; + + public constructor(private configuration: IConfiguration) { } + + public resetCache(): void { + this._mapToParams = undefined; + this._mapToTemplates = undefined; + } + + private ensureMapsCreated(): void { + if (this._mapToParams && this._mapToTemplates) { + return; + } + + this._mapToParams = new Map(); + this._mapToTemplates = new Map(); + + const paramFiles: { [key: string]: unknown } | undefined = + this.configuration.get<{ [key: string]: unknown }>(configKeys.parameterFiles) + // tslint:disable-next-line: strict-boolean-expressions + || {}; + if (typeof paramFiles === 'object') { + for (let templatePath of Object.getOwnPropertyNames(paramFiles)) { + let paramPathObject = paramFiles[templatePath]; + if (typeof paramPathObject !== 'string' || isNullOrUndefined(paramPathObject)) { + continue; + } + const paramPath: string = paramPathObject; + + const resolvedTemplatePath = path.resolve(templatePath); + const normalizedTemplatePath: string = normalizePath(resolvedTemplatePath); + + if (path.isAbsolute(templatePath) && isFilePath(templatePath)) { + // Resolve parameter file relative to template file's folder + let resolvedParamPath: string = resolveParameterFilePath(normalizedTemplatePath, paramPath); + if (isFilePath(resolvedParamPath)) { + // If the user has an entry in both workspace and user settings, vscode combines the two objects, + // with workspace settings overriding the user settings. + // If there are two entries differing only by case, allow the last one to win, because it will be + // the workspace setting value. + // Therefore replacing any previous values found. + this._mapToParams.set(normalizedTemplatePath, { + resolvedTemplate: Uri.file(resolvedTemplatePath), + normalizedTemplate: Uri.file(normalizedTemplatePath), + resolvedParams: Uri.file(resolvedParamPath), + normalizedParams: Uri.file(normalizePath(resolvedParamPath)) + }); + } + } + } + } + + // Create reverse mapping + for (let entry of this._mapToParams) { + const mapping: IMapping = entry[1]; + this._mapToTemplates.set(mapping.normalizedParams.fsPath, mapping); + } + } + + /** + * Given a template file, find the parameter file, if any, that the user currently has associated with it. + */ + public getParameterFile(templateFileUri: Uri): Uri | undefined { + this.ensureMapsCreated(); + const normalizedTemplatePath = normalizePath(templateFileUri); + const entry = this._mapToParams?.get(normalizedTemplatePath); + return entry?.resolvedParams; + } + + public getTemplateFile(parameterFileUri: Uri): Uri | undefined { + this.ensureMapsCreated(); + const normalizedParamPath = normalizePath(parameterFileUri); + const entry = this._mapToTemplates?.get(normalizedParamPath); + return entry?.resolvedTemplate; + } + + /** + * Sets a mapping from a template file to a parameter file + */ + public async mapParameterFile(templateUri: Uri, paramFileUri: Uri | undefined): Promise { + const relativeParamFilePath: string | undefined = paramFileUri ? getRelativeParameterFilePath(templateUri, paramFileUri) : undefined; + const normalizedTemplatePath = normalizePath(templateUri.fsPath); + + // We want to adjust the collection in the user settings, ignoring anything in the workspace settings + let map = this.configuration + .inspect<{ [key: string]: string | undefined }>(configKeys.parameterFiles)?.globalValue + // tslint:disable-next-line: strict-boolean-expressions + || {}; + + if (typeof map !== 'object') { + map = {}; + } + + // Copy existing entries that don't match (might be multiple entries with different casing, so can't do simple delete) + const newMap: { [key: string]: string | undefined } = {}; + + for (let templatePath of Object.getOwnPropertyNames(map)) { + if (normalizePath(templatePath) !== normalizedTemplatePath) { + newMap[templatePath] = map[templatePath]; + } + } + + // Add new entry + if (paramFileUri) { + newMap[normalizedTemplatePath] = relativeParamFilePath; + } + + await this.configuration.update(configKeys.parameterFiles, newMap, ConfigurationTarget.Global); + this.resetCache(); + } +} + +function isFilePath(p: string): boolean { + const resolved = path.resolve(p); + return !!resolved.match(/[^./\\]/); +} diff --git a/src/parameterFiles/DeploymentParameters.ts b/src/parameterFiles/DeploymentParameters.ts new file mode 100644 index 000000000..e600c9729 --- /dev/null +++ b/src/parameterFiles/DeploymentParameters.ts @@ -0,0 +1,298 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import * as assert from 'assert'; +import { EOL } from "os"; +import { CodeAction, CodeActionContext, CodeActionKind, Command, Range, Selection, TextEditor, Uri } from "vscode"; +import { CachedValue } from "../CachedValue"; +import { templateKeys } from "../constants"; +import { DeploymentDocument } from "../DeploymentDocument"; +import { DeploymentTemplate } from "../DeploymentTemplate"; +import { INamedDefinition } from "../INamedDefinition"; +import { IParameterDefinition } from "../IParameterDefinition"; +import * as Json from "../JSON"; +import * as language from "../Language"; +import { createParameterFromTemplateParameter, defaultTabSize } from "../parameterFileGeneration"; +import { ReferenceList } from "../ReferenceList"; +import { isParametersSchema } from "../schemas"; +import { indentMultilineString } from "../util/multilineStrings"; +import { getVSCodePositionFromPosition, getVSCodeRangeFromSpan } from "../util/vscodePosition"; +import { ParametersPositionContext } from "./ParametersPositionContext"; +import { ParameterValueDefinition } from "./ParameterValueDefinition"; + +/** + * Represents a deployment parameter file + */ +export class DeploymentParameters extends DeploymentDocument { + private _parameterValueDefinitions: CachedValue = new CachedValue(); + private _parametersProperty: CachedValue = new CachedValue(); + + /** + * Create a new DeploymentParameters instance + * + * @param _documentText The string text of the document. + * @param _documentId A unique identifier for this document. Usually this will be a URI to the document. + */ + constructor(documentText: string, documentId: Uri) { + super(documentText, documentId); + } + + public hasParametersUri(): boolean { + return isParametersSchema(this.schemaUri); + } + + // case-insensitive + public getParameterValue(parameterName: string): ParameterValueDefinition | undefined { + // Number of parameters generally small, not worth creating a case-insensitive dictionary + const parameterNameLC = parameterName.toLowerCase(); + for (let param of this.parameterValues) { + if (param.nameValue.unquotedValue.toLowerCase() === parameterNameLC) { + return param; + } + } + + return undefined; + } + + public get parameterValues(): ParameterValueDefinition[] { + return this._parameterValueDefinitions.getOrCacheValue(() => { + const parameterDefinitions: ParameterValueDefinition[] = []; + + // tslint:disable-next-line: strict-boolean-expressions + for (const parameter of this.parametersObjectValue?.properties || []) { + parameterDefinitions.push(new ParameterValueDefinition(parameter)); + } + + return parameterDefinitions; + }); + } + + public get parametersProperty(): Json.Property | undefined { + return this._parametersProperty.getOrCacheValue(() => { + return this.topLevelValue?.getProperty(templateKeys.parameters); + }); + } + + public get parametersObjectValue(): Json.ObjectValue | undefined { + return Json.asObjectValue(this.parametersProperty?.value); + } + + public getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number, associatedTemplate: DeploymentTemplate | undefined): ParametersPositionContext { + return ParametersPositionContext.fromDocumentLineAndColumnIndices(this, documentLineIndex, documentColumnIndex, associatedTemplate); + } + + public getContextFromDocumentCharacterIndex(documentCharacterIndex: number, associatedDocument: DeploymentTemplate | undefined): ParametersPositionContext { + return ParametersPositionContext.fromDocumentCharacterIndex(this, documentCharacterIndex, associatedDocument); + } + + public findReferencesToDefinition(definition: INamedDefinition): ReferenceList { + const results: ReferenceList = new ReferenceList(definition.definitionKind); + + // The only reference possible in the parameter file is the parameter's value definition + if (definition.nameValue) { + const paramValue = this.getParameterValue(definition.nameValue.unquotedValue); + if (paramValue) { + results.add({ document: this, span: paramValue.nameValue.unquotedSpan }); + } + } + return results; + } + + public async getCodeActions( + associatedDocument: DeploymentDocument | undefined, + range: Range | Selection, + context: CodeActionContext + ): Promise<(Command | CodeAction)[]> { + assert(!associatedDocument || associatedDocument instanceof DeploymentTemplate, "Associated document is of the wrong type"); + const template: DeploymentTemplate | undefined = associatedDocument; + + const actions: (Command | CodeAction)[] = []; + const parametersProperty = this.parametersProperty; + + if (parametersProperty) { + const lineIndex = this.getDocumentPosition(parametersProperty?.nameValue.span.startIndex).line; + if (lineIndex >= range.start.line && lineIndex <= range.end.line) { + const missingParameters: IParameterDefinition[] = this.getMissingParameters(template, false); + + // Add missing required parameters + if (missingParameters.some(p => this.isParameterRequired(p))) { + const action = new CodeAction("Add missing required parameters", CodeActionKind.QuickFix); + action.command = { + command: 'azurerm-vscode-tools.codeAction.addMissingRequiredParameters', + title: action.title, + arguments: [ + this.documentId + ] + }; + actions.push(action); + } + + // Add all missing parameters + if (missingParameters.length > 0) { + const action = new CodeAction("Add all missing parameters", CodeActionKind.QuickFix); + action.command = { + command: 'azurerm-vscode-tools.codeAction.addAllMissingParameters', + title: action.title, + arguments: [ + this.documentId + ] + }; + actions.push(action); + } + } + } + + return actions; + } + + private isParameterRequired(paramDef: IParameterDefinition): boolean { + return !paramDef.defaultValue; + } + + private getMissingParameters(template: DeploymentTemplate | undefined, onlyRequiredParameters: boolean): IParameterDefinition[] { + if (!template) { + return []; + } + + const results: IParameterDefinition[] = []; + for (let paramDef of template.topLevelScope.parameterDefinitions) { + const paramValue = this.getParameterValue(paramDef.nameValue.unquotedValue); + if (!paramValue) { + results.push(paramDef); + } + } + + if (onlyRequiredParameters) { + return results.filter(p => this.isParameterRequired(p)); + } + + return results; + } + + public async addMissingParameters( + editor: TextEditor, + template: DeploymentTemplate, + onlyRequiredParameters: boolean + ): Promise { + // Find the location to insert new stuff in the parameters section + if (this.parametersProperty && this.parametersObjectValue) { + // Where insert? + // Find last non-whitespace token inside the parameters section + let lastTokenInParameters: Json.Token | undefined; + for (let i = this.parametersProperty.span.endIndex - 1; // Start before the closing "}" + i >= this.parametersProperty.span.startIndex; + --i) { + lastTokenInParameters = this.jsonParseResult.getTokenAtCharacterIndex(i, Json.Comments.includeCommentTokens); + if (lastTokenInParameters) { + break; + } + } + const insertIndex: number = lastTokenInParameters + ? lastTokenInParameters.span.afterEndIndex + : this.parametersObjectValue.span.endIndex; + const insertPosition = this.getDocumentPosition(insertIndex); + + // Find missing params + const missingParams: IParameterDefinition[] = this.getMissingParameters(template, onlyRequiredParameters); + if (missingParams.length === 0) { + return; + } + + // Create insertion text + let paramsAsText: string[] = []; + for (let param of missingParams) { + const paramText = createParameterFromTemplateParameter(template, param, defaultTabSize); + paramsAsText.push(paramText); + } + let newText = paramsAsText.join(`,${EOL}`); + + // Determine indentation + const parametersObjectIndent = this.getDocumentPosition(this.parametersProperty?.nameValue.span.startIndex).column; + const lastParameter = this.parameterValues.length > 0 ? this.parameterValues[this.parameterValues.length - 1] : undefined; + const lastParameterIndent = lastParameter ? this.getDocumentPosition(lastParameter?.fullSpan.startIndex).column : undefined; + const newTextIndent = lastParameterIndent === undefined ? parametersObjectIndent + defaultTabSize : lastParameterIndent; + let indentedText = indentMultilineString(newText, newTextIndent); + let insertText = EOL + indentedText; + + // If insertion point is on the same line as the end of the parameters object, then add a newline + // afterwards and indent it (e.g. parameters object = empty, {}) + if (this.getDocumentPosition(insertIndex).line + === this.getDocumentPosition(this.parametersObjectValue.span.endIndex).line + ) { + insertText += EOL + ' '.repeat(defaultTabSize); + } + + // Add comma before? + let commaEdit = this.createEditToAddCommaBeforePosition(insertIndex); + assert(!commaEdit || commaEdit.span.endIndex <= insertIndex); + if (commaEdit?.span.startIndex === insertIndex) { + // vscode doesn't like both edits starting at the same location, so + // just add the comma directly to the string (this is the common case) + commaEdit = undefined; + insertText = `,${insertText}`; + } + + await editor.edit(editBuilder => { + + editBuilder.insert(getVSCodePositionFromPosition(insertPosition), insertText); + if (commaEdit) { + editBuilder.replace( + getVSCodeRangeFromSpan(this, commaEdit.span), + commaEdit.insertText); + } + }); + } + } + + public createEditToAddCommaBeforePosition(documentIndex: number): { insertText: string; span: language.Span } | undefined { + // Are there are any parameters before the one being inserted? + const newParamIndex = this.parameterValues + .filter( + p => p.fullSpan.endIndex < documentIndex) + .length; + if (newParamIndex > 0) { + const prevParameter = this.parameterValues[newParamIndex - 1]; + assert(prevParameter); + + // Is there already a comma after the last parameter? + const firstIndexAfterPrev = prevParameter.fullSpan.afterEndIndex; + const tokensBetweenParams = this.jsonParseResult.getTokensInSpan( + new language.Span( + firstIndexAfterPrev, + documentIndex - firstIndexAfterPrev), + Json.Comments.ignoreCommentTokens + ); + if (tokensBetweenParams.some(t => t.type === Json.TokenType.Comma)) { + // ... yes + return undefined; + } + + // Insert a new comma right after last item's full span + const insertIndex = prevParameter.fullSpan.afterEndIndex; + return { + insertText: ',', + span: new language.Span(insertIndex, 0) + }; + } + + return undefined; + } + + // CONSIDER: This cache depends on associatedTemplate not changing (which it shouldn't) + public async getErrors(associatedTemplate: DeploymentTemplate | undefined): Promise { + const missingRequiredParams: IParameterDefinition[] = this.getMissingParameters(associatedTemplate, true); + if (missingRequiredParams.length === 0) { + return []; + } + + const missingParamNames = missingRequiredParams.map(param => `"${param.nameValue.unquotedValue}"`); + const message = `The following parameters do not have default values and require a value in the parameter file: ${missingParamNames.join(', ')}`; + const span = this.parametersProperty?.nameValue.span ?? new language.Span(0, 0); + return [new language.Issue(span, message, language.IssueKind.params_missingRequiredParam)]; + } + + public getWarnings(): language.Issue[] { + return []; + } +} diff --git a/src/parameterFiles/ParameterValueDefinition.ts b/src/parameterFiles/ParameterValueDefinition.ts new file mode 100644 index 000000000..77eb4b7ea --- /dev/null +++ b/src/parameterFiles/ParameterValueDefinition.ts @@ -0,0 +1,56 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { assert } from '../fixed_assert'; +import { IUsageInfo } from '../Hover'; +import { DefinitionKind, INamedDefinition } from '../INamedDefinition'; +import * as Json from "../JSON"; +import * as language from "../Language"; + +export function isParameterValueDefinition(definition: INamedDefinition): definition is ParameterValueDefinition { + return definition.definitionKind === DefinitionKind.ParameterValue; +} + +/** + * This class represents the definition of a parameter value in a deployment parameter file + */ +export class ParameterValueDefinition implements INamedDefinition { + public readonly definitionKind: DefinitionKind = DefinitionKind.Parameter; + + constructor(private readonly _property: Json.Property) { + assert(_property); + } + + public get nameValue(): Json.StringValue { + return this._property.nameValue; + } + + public get fullSpan(): language.Span { + return this._property.span; + } + + public get value(): Json.Value | undefined { + const parameterValue: Json.ObjectValue | undefined = Json.asObjectValue(this._property.value); + if (parameterValue) { + return parameterValue.getPropertyValue("value"); + } + + return undefined; + } + + public get usageInfo(): IUsageInfo { + return { + usage: this.nameValue.unquotedValue, + friendlyType: "parameter value", + description: "Parameter value" + }; + } + + /** + * Convenient way of seeing what this object represents in the debugger, shouldn't be used for production code + */ + public get __debugDisplay(): string { + return `${this.nameValue.toString()} = ${this.value?.__debugDisplay}`; + } +} diff --git a/src/parameterFiles/ParametersPositionContext.ts b/src/parameterFiles/ParametersPositionContext.ts new file mode 100644 index 000000000..85e1e725f --- /dev/null +++ b/src/parameterFiles/ParametersPositionContext.ts @@ -0,0 +1,236 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { EOL } from "os"; +import * as Completion from "../Completion"; +import { DeploymentTemplate } from "../DeploymentTemplate"; +import * as language from "../Language"; +import { createParameterFromTemplateParameter } from "../parameterFileGeneration"; +import { IReferenceSite, PositionContext, ReferenceSiteKind } from "../PositionContext"; +import { ReferenceList } from "../ReferenceList"; +import * as TLE from '../TLE'; +import { DeploymentParameters } from "./DeploymentParameters"; + +/** + * Represents a position inside the snapshot of a deployment parameter file, plus all related information + * that can be parsed and analyzed about it from that position. + */ +export class ParametersPositionContext extends PositionContext { + // CONSIDER: pass in function to *get* the deployment template, not the template itself? + private _associatedTemplate: DeploymentTemplate | undefined; + + private constructor(deploymentParameters: DeploymentParameters, associatedTemplate: DeploymentTemplate | undefined) { + super(deploymentParameters, associatedTemplate); + this._associatedTemplate = associatedTemplate; + } + + public static fromDocumentLineAndColumnIndices(deploymentParameters: DeploymentParameters, documentLineIndex: number, documentColumnIndex: number, associatedTemplate: DeploymentTemplate | undefined): ParametersPositionContext { + let context = new ParametersPositionContext(deploymentParameters, associatedTemplate); + context.initFromDocumentLineAndColumnIndices(documentLineIndex, documentColumnIndex); + return context; + } + public static fromDocumentCharacterIndex(deploymentParameters: DeploymentParameters, documentCharacterIndex: number, deploymentTemplate: DeploymentTemplate | undefined): ParametersPositionContext { + let context = new ParametersPositionContext(deploymentParameters, deploymentTemplate); + context.initFromDocumentCharacterIndex(documentCharacterIndex); + return context; + } + + public get document(): DeploymentParameters { + return super.document; + } + + /** + * If this position is inside an expression, inside a reference to an interesting function/parameter/etc, then + * return an object with information about this reference and the corresponding definition + */ + public getReferenceSiteInfo(_includeDefinition: boolean): IReferenceSite | undefined { + if (!this._associatedTemplate) { + return undefined; + } + + for (let paramValue of this.document.parameterValues) { + // Are we inside the name of a parameter? + if (paramValue.nameValue.span.contains(this.documentCharacterIndex, language.Contains.extended)) { + // Does it have an associated parameter definition in the template? + const paramDef = this._associatedTemplate?.topLevelScope.getParameterDefinition(paramValue.nameValue.unquotedValue); + if (paramDef) { + return { + referenceKind: ReferenceSiteKind.reference, + referenceSpan: paramValue.nameValue.unquotedSpan, + referenceDocument: this.document, + definition: paramDef, + definitionDocument: this._associatedTemplate + }; + } + + break; + } + } + + return undefined; + } + + /** + * Return all references to the given reference site info in this document + * @returns undefined if references are not supported at this location, or empty list if supported but none found + */ + protected getReferencesCore(): ReferenceList | undefined { + const refInfo = this.getReferenceSiteInfo(false); + return refInfo ? this.document.findReferencesToDefinition(refInfo.definition) : undefined; + } + + public getCompletionItems(): Completion.Item[] { + let completions: Completion.Item[] = []; + + if (this.canAddPropertyHere) { + completions.push(... this.getCompletionsForMissingParameters()); + completions.push(this.getCompletionForNewParameter()); + } + + return completions; + } + + private getCompletionForNewParameter(): Completion.Item { + const detail = "Insert new parameter"; + let snippet = + // tslint:disable-next-line:prefer-template + `"\${1:parameter1}": {` + EOL + + `\t"value": "\${2:value}"` + EOL + + `}`; + const documentation = "documentation"; + const label = `""`; + + return this.createParameterCompletion( + label, + snippet, + Completion.CompletionKind.NewPropertyValue, + detail, + documentation); + } + + /** + * Get completion items for our position in the document + */ + private getCompletionsForMissingParameters(): Completion.Item[] { + const completions: Completion.Item[] = []; + if (this._associatedTemplate) { + const paramsInParameterFile: string[] = this.document.parameterValues.map( + pv => pv.nameValue.unquotedValue.toLowerCase()); + + // For each parameter in the template + for (let param of this._associatedTemplate.topLevelScope.parameterDefinitions) { + // Is this already in the parameter file? + const paramNameLC = param.nameValue.unquotedValue.toLowerCase(); + if (paramsInParameterFile.includes(paramNameLC)) { + continue; + } + + // tslint:disable-next-line:prefer-template + const isRequired = !param.defaultValue; + const label = `${param.nameValue.quotedValue} ${isRequired ? "(required)" : "(optional)"}`; + const paramText = createParameterFromTemplateParameter(this._associatedTemplate, param); + let replacement = paramText; + const documentation = `Insert a value for parameter '${param.nameValue.unquotedValue}' from the template file"`; + const detail = paramText; + + completions.push( + this.createParameterCompletion( + label, + replacement, + Completion.CompletionKind.PropertyValue, + detail, + documentation)); + } + } + + return completions; + } + + private createParameterCompletion( + label: string, + replacement: string, + kind: Completion.CompletionKind, + detail: string, + documentation: string + ): Completion.Item { + // Replacement span + let span = this.determineCompletionSpan(); + + // Comma after? + if (this.needsCommaAfterCompletion()) { + replacement += ','; + } + + // Comma before? + const commaEdit = this.document.createEditToAddCommaBeforePosition(this.documentCharacterIndex); + + return new Completion.Item( + label, + replacement, + span, + kind, + detail, + documentation, + undefined, + commaEdit ? [commaEdit] : undefined); + } + + private determineCompletionSpan(): language.Span { + let span = this.emptySpanAtDocumentCharacterIndex; + + // If the completion is triggered inside double quotes, or from a trigger character of a double quotes ( + // which ends up adding '""' first, then triggering the completion inside the quotes), then + // the insert range needs to subsume those quotes so they get deleted when the new param is inserted. + if (this.document.documentText.charAt(this.documentCharacterIndex - 1) === '"') { + span = span.extendLeft(1); + } + if (this.document.documentText.charAt(this.documentCharacterIndex) === '"') { + span = span.extendRight(1); + } + + return span; + } + + private needsCommaAfterCompletion(): boolean { + // If there are any parameters after the one being inserted, we need to add a comma after the new one + if (this.document.parameterValues.some(p => p.fullSpan.startIndex >= this.documentCharacterIndex)) { + return true; + } + + return false; + } + + // True if inside the "parameters" object, but not inside any properties + // within it. + public get canAddPropertyHere(): boolean { + if (!this.document.parametersObjectValue) { + // No "parameters" section + return false; + } + + const enclosingJsonValue = this.document.jsonParseResult.getValueAtCharacterIndex( + this.documentCharacterIndex, + language.Contains.enclosed); + + if (enclosingJsonValue !== this.document.parametersObjectValue) { + // Directly-enclosing JSON value/object at the cursor is not the "parameters" object + // (either it's outside it, or it's within a subvalue like an existing parameter) + return false; + } + + // Check if we're inside a comment + if (!!this.document.jsonParseResult.getCommentTokenAtDocumentIndex( + this.documentCharacterIndex, + language.Contains.enclosed) + ) { + return false; + } + + return true; + } + + public getSignatureHelp(): TLE.FunctionSignatureHelp | undefined { + return undefined; + } +} diff --git a/src/parameterFiles.ts b/src/parameterFiles/parameterFiles.ts similarity index 74% rename from src/parameterFiles.ts rename to src/parameterFiles/parameterFiles.ts index f991d4f8b..890766720 100644 --- a/src/parameterFiles.ts +++ b/src/parameterFiles/parameterFiles.ts @@ -5,13 +5,15 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import * as path from 'path'; -import { commands, ConfigurationTarget, MessageItem, TextDocument, Uri, ViewColumn, window, workspace } from 'vscode'; +import { commands, MessageItem, TextDocument, Uri, window, workspace } from 'vscode'; import { callWithTelemetryAndErrorHandling, DialogResponses, IActionContext, IAzureQuickPickItem, UserCancelledError } from 'vscode-azureextensionui'; -import { configKeys, configPrefix, globalStateKeys, isWin32 } from './constants'; -import { DeploymentTemplate } from './DeploymentTemplate'; -import { queryCreateParameterFile } from './editParameterFile'; -import { ext } from './extensionVariables'; -import { containsParametersSchema } from './schemas'; +import { configKeys, configPrefix, globalStateKeys } from '../constants'; +import { DeploymentTemplate } from '../DeploymentTemplate'; +import { ext } from '../extensionVariables'; +import { queryCreateParameterFile } from '../parameterFileGeneration'; +import { containsParametersSchema } from '../schemas'; +import { normalizePath } from '../util/normalizePath'; +import { DeploymentFileMapping } from './DeploymentFileMapping'; const readAtMostBytesToFindParamsSchema = 4 * 1024; const currentMessage = "Current"; @@ -33,7 +35,7 @@ interface IPossibleParameterFile { } // tslint:disable-next-line: max-func-body-length -export async function selectParameterFile(actionContext: IActionContext, sourceUri: Uri | undefined): Promise { +export async function selectParameterFile(actionContext: IActionContext, mapping: DeploymentFileMapping, sourceUri: Uri | undefined): Promise { if (!sourceUri) { sourceUri = window.activeTextEditor?.document.uri; } @@ -46,12 +48,12 @@ export async function selectParameterFile(actionContext: IActionContext, sourceU // Verify it's a template file (have to read in entire file to do full validation) const contents = (await fse.readFile(templateUri.fsPath, { encoding: "utf8" })).toString(); - const template: DeploymentTemplate = new DeploymentTemplate(contents, "Check file is template"); + const template: DeploymentTemplate = new DeploymentTemplate(contents, Uri.file("https://Check file is template")); if (!template.hasArmSchemaUri()) { throw new Error(`"${templateUri.fsPath}" does not appear to be an Azure Resource Manager deployment template file.`); } - let quickPickList: IQuickPickList = await createParameterFileQuickPickList(templateUri); + let quickPickList: IQuickPickList = await createParameterFileQuickPickList(mapping, templateUri); // Show the quick pick const result: IAzureQuickPickItem = await ext.ui.showQuickPick( quickPickList.items, @@ -67,7 +69,7 @@ export async function selectParameterFile(actionContext: IActionContext, sourceU // Remove the mapping for this file await neverAskAgain(templateUri, actionContext); - await setMappedParameterFileForTemplate(templateUri, undefined); + await mapping.mapParameterFile(templateUri, undefined); } else if (result === quickPickList.browse) { // Browse... @@ -96,12 +98,12 @@ export async function selectParameterFile(actionContext: IActionContext, sourceU await neverAskAgain(templateUri, actionContext); // Map to the browsed file - await setMappedParameterFileForTemplate(templateUri, selectedParamsPath); + await mapping.mapParameterFile(templateUri, selectedParamsPath); } else if (result === quickPickList.newFile) { // New parameter file let newUri: Uri = await queryCreateParameterFile(actionContext, templateUri, template); - await setMappedParameterFileForTemplate(templateUri, newUri); + await mapping.mapParameterFile(templateUri, newUri); await commands.executeCommand('azurerm-vscode-tools.openParameterFile', templateUri, newUri); } else if (result === quickPickList.openCurrent) { // Open current @@ -117,35 +119,61 @@ export async function selectParameterFile(actionContext: IActionContext, sourceU assert(result.data, "Quick pick item should have had data"); await neverAskAgain(templateUri, actionContext); - await setMappedParameterFileForTemplate(templateUri, result.data?.uri); + await mapping.mapParameterFile(templateUri, result.data?.uri); } } -export async function openParameterFile(actionContext: IActionContext, sourceUri?: Uri, parameterUri?: Uri): Promise { - if (sourceUri) { - let paramFile: Uri | undefined = parameterUri || findMappedParameterFileForTemplate(sourceUri); +export async function openParameterFile(mapping: DeploymentFileMapping, templateUri: Uri | undefined, parameterUri: Uri | undefined): Promise { + if (templateUri) { + let paramFile: Uri | undefined = parameterUri || mapping.getParameterFile(templateUri); if (!paramFile) { - throw new Error(`There is currently no parameter file for template file "${sourceUri.fsPath}"`); + throw new Error(`There is currently no parameter file for template file "${templateUri.fsPath}"`); } let doc: TextDocument = await workspace.openTextDocument(paramFile); - await window.showTextDocument(doc, ViewColumn.Beside); + await window.showTextDocument(doc); + } +} + +export async function openTemplateFile(mapping: DeploymentFileMapping, parameterUri: Uri | undefined, templateUri: Uri | undefined): Promise { + if (parameterUri) { + let templateFile: Uri | undefined = templateUri || mapping.getTemplateFile(parameterUri); + if (!templateFile) { + throw new Error(`There is no template file currently associated with parameter file "${parameterUri.fsPath}"`); + } + + let doc: TextDocument = await workspace.openTextDocument(templateFile); + await window.showTextDocument(doc); } } /** - * If the params file is inside the workspace folder, use the path relative to its template file. Otherwise, return the - * absolute path to the params file. This is intended to make the path most logical to the user. + * If the file is inside the workspace folder, use the path relative to that, otherwise + * use the absolute path. This is intended for UI only. */ -export function getFriendlyPathToParameterFile(templateUri: Uri, paramFileUri: Uri): string { - const workspaceFolder = workspace.getWorkspaceFolder(paramFileUri); +export function getFriendlyPathToFile(uri: Uri): string { + const workspaceFolder = workspace.getWorkspaceFolder(uri); + if (workspaceFolder) { - return path.relative(path.dirname(templateUri.fsPath), paramFileUri.fsPath); + return path.relative(workspaceFolder.uri.fsPath, uri.fsPath); } else { - return paramFileUri.fsPath; + return uri.fsPath; } } +export function getRelativeParameterFilePath(templateUri: Uri, parameterUri: Uri): string { + const templatePath = normalizePath(templateUri); + const paramPath = normalizePath(parameterUri); + + return path.relative(path.dirname(templatePath), paramPath); +} + +export function resolveParameterFilePath(templatePath: string, parameterPathRelativeToTemplate: string): string { + assert(path.isAbsolute(templatePath)); + const resolved = path.resolve(path.dirname(templatePath), parameterPathRelativeToTemplate); + return resolved; +} + interface IQuickPickList { items: IAzureQuickPickItem[]; currentParamFile: IPossibleParameterFile | undefined; @@ -155,17 +183,17 @@ interface IQuickPickList { openCurrent: IAzureQuickPickItem; } -async function createParameterFileQuickPickList(templateUri: Uri): Promise { +async function createParameterFileQuickPickList(mapping: DeploymentFileMapping, templateUri: Uri): Promise { // Find likely parameter file matches let suggestions: IPossibleParameterFile[] = await findSuggestedParameterFiles(templateUri); // Find the current in that list - const currentParamUri: Uri | undefined = findMappedParameterFileForTemplate(templateUri); + const currentParamUri: Uri | undefined = mapping.getParameterFile(templateUri); const currentParamPathNormalized: string | undefined = currentParamUri ? normalizePath(currentParamUri) : undefined; let currentParamFile: IPossibleParameterFile | undefined = suggestions.find(pf => normalizePath(pf.uri) === currentParamPathNormalized); if (currentParamUri && !currentParamFile) { // There is a current parameter file, but it wasn't among the list we came up with. We must add it to the list. - currentParamFile = { isCloseNameMatch: false, uri: currentParamUri, friendlyPath: getFriendlyPathToParameterFile(templateUri, currentParamUri) }; + currentParamFile = { isCloseNameMatch: false, uri: currentParamUri, friendlyPath: getRelativeParameterFilePath(templateUri, currentParamUri) }; let exists = false; try { exists = await fse.pathExists(currentParamUri.fsPath); @@ -250,17 +278,6 @@ function createQuickPickItem(paramFile: IPossibleParameterFile, current: IPossib }; } -function normalizePath(filePath: Uri | string): string { - const fsPath: string = typeof filePath === 'string' ? filePath : - filePath.fsPath; - let normalizedPath = path.normalize(fsPath); - if (isWin32) { - normalizedPath = normalizedPath.toLowerCase(); - } - - return normalizedPath; -} - /** * Finds parameter files to suggest for a given template. */ @@ -281,7 +298,7 @@ export async function findSuggestedParameterFiles(templateUri: Uri): Promise => { @@ -415,7 +432,7 @@ export function considerQueryingForParameterFile(document: TextDocument): void { switch (response.title) { case yes.title: - await setMappedParameterFileForTemplate(templateUri, closestMatch.uri); + await mapping.mapParameterFile(templateUri, closestMatch.uri); break; case no.title: // We won't ask again @@ -471,72 +488,6 @@ async function neverAskAgain(templateUri: Uri, actionContext: IActionContext): P await ext.context.globalState.update(globalStateKeys.dontAskAboutParameterFiles, neverAskFiles); } -/** - * Given a template file, find the parameter file, if any, that the user currently has associated with it - */ -export function findMappedParameterFileForTemplate(templateFileUri: Uri): Uri | undefined { - const paramFiles: { [key: string]: string } | undefined = - workspace.getConfiguration(configPrefix).get<{ [key: string]: string }>(configKeys.parameterFiles) - // tslint:disable-next-line: strict-boolean-expressions - || {}; - if (typeof paramFiles === 'object') { - const normalizedTemplatePath = normalizePath(templateFileUri.fsPath); - let paramFile: Uri | undefined; - - // Can't do a simple lookup because need to be case-insensitivity tolerant on Win32 - for (let fileNameKey of Object.getOwnPropertyNames(paramFiles)) { - const normalizedFileName: string | undefined = normalizePath(fileNameKey); - if (normalizedFileName === normalizedTemplatePath) { - if (typeof paramFiles[fileNameKey] === 'string') { - // Resolve relative to template file's folder - let resolvedPath = path.resolve(path.dirname(templateFileUri.fsPath), paramFiles[fileNameKey]); - - // If the user has an entry in both workspace and user settings, vscode combines the two objects, - // with workspace settings overriding the user settings. - // If there are two entries differing only by case, allow the last one to win, because it will be - // the workspace setting value - paramFile = !!resolvedPath ? Uri.file(resolvedPath) : undefined; - } - } - } - - return paramFile; - } - - return undefined; -} - -async function setMappedParameterFileForTemplate(templateUri: Uri, paramFileUri: Uri | undefined): Promise { - const relativeParamFilePath: string | undefined = paramFileUri ? getFriendlyPathToParameterFile(templateUri, paramFileUri) : undefined; - const normalizedTemplatePath = normalizePath(templateUri.fsPath); - - // We only want the values in the user settings - const map = workspace.getConfiguration(configPrefix) - .inspect<{ [key: string]: string | undefined }>(configKeys.parameterFiles)?.globalValue - // tslint:disable-next-line: strict-boolean-expressions - || {}; - - if (typeof map !== 'object') { - return; - } - - // Copy existing entries that don't match (might be multiple entries with different casing, so can't do simple delete) - const newMap: { [key: string]: string | undefined } = {}; - - for (let templatePath of Object.getOwnPropertyNames(map)) { - if (normalizePath(templatePath) !== normalizedTemplatePath) { - newMap[templatePath] = map[templatePath]; - } - } - - // Add new entry - if (paramFileUri) { - newMap[templateUri.fsPath] = relativeParamFilePath; - } - - await workspace.getConfiguration(configPrefix).update(configKeys.parameterFiles, newMap, ConfigurationTarget.Global); -} - function hasSupportedParameterFileExtension(filePath: string): boolean { const extension = path.extname(filePath).toLowerCase(); return extension === '.json' || extension === '.jsonc'; diff --git a/src/parameterFiles/setParameterFileContext.ts b/src/parameterFiles/setParameterFileContext.ts new file mode 100644 index 000000000..f456f8a15 --- /dev/null +++ b/src/parameterFiles/setParameterFileContext.ts @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { commands } from 'vscode'; +import { assert } from '../fixed_assert'; + +// These contexts are used to drive when/enablement clauses in package.json + +// Is current file a parameter file? +const isParameterFileContextName = `azurerm-vscode-tools-isParamFile`; + +// Is a parameter file and has an associated template file? +const hasTemplateFileContextName = `azurerm-vscode-tools-hasTemplateFile`; + +// Is a template file and has an associated parameter file? +const hasParameterFileContextName = `azurerm-vscode-tools-hasParamFile`; + +export function setParameterFileContext( + value: { + isTemplateFile: boolean; + hasParamFile: boolean; + + isParamFile: boolean; + hasTemplateFile: boolean; + }): void { + assert(!(value.isTemplateFile && value.isParamFile)); + assert(!(!value.isTemplateFile && value.hasParamFile)); + assert(!(!value.isParamFile && value.hasTemplateFile)); + + // Don't wait for return for any of these... + + // Temlate files... + // Note: We don't need an "isTemplateFile" context because we have a specific langid for template files + commands.executeCommand( + 'setContext', + hasParameterFileContextName, + value.isTemplateFile && value.hasParamFile); + + // Parameter files... + commands.executeCommand( + 'setContext', + isParameterFileContextName, + value.isParamFile); + commands.executeCommand( + 'setContext', + hasTemplateFileContextName, + value.hasTemplateFile); +} diff --git a/src/supported.ts b/src/supported.ts index 987641faf..f70d7787f 100644 --- a/src/supported.ts +++ b/src/supported.ts @@ -3,12 +3,28 @@ // ---------------------------------------------------------------------------- import { Position, Range, TextDocument, workspace } from "vscode"; -import { configKeys, configPrefix, languageId } from "./constants"; -import { containsArmSchema } from "./schemas"; +import { armTemplateLanguageId, configKeys, configPrefix } from "./constants"; +import { containsArmSchema, containsParametersSchema } from "./schemas"; -export const armDeploymentDocumentSelector = [ - { language: languageId, scheme: 'file' }, - { language: languageId, scheme: 'untitled' } // unsaved files +export const templateDocumentSelector = [ + { language: armTemplateLanguageId, scheme: 'file' }, + { language: armTemplateLanguageId, scheme: 'untitled' } // unsaved files +]; + +export const parameterDocumentSelector = [ + { language: 'json', scheme: 'file' }, + { language: 'json', scheme: 'untitled' }, + { language: 'jsonc', scheme: 'file' }, + { language: 'jsonc', scheme: 'untitled' }, +]; + +export const templateOrParameterDocumentSelector = [ + { language: armTemplateLanguageId, scheme: 'file' }, + { language: armTemplateLanguageId, scheme: 'untitled' }, // unsaved files + { language: 'json', scheme: 'file' }, + { language: 'json', scheme: 'untitled' }, + { language: 'jsonc', scheme: 'file' }, + { language: 'jsonc', scheme: 'untitled' }, ]; const maxLinesToDetectSchemaIn = 500; @@ -18,7 +34,7 @@ function isJsonOrJsoncLangId(textDocument: TextDocument): boolean { } // We keep track of arm-template files, of course, -// but also JSON/JSONC (unless auto-detect is disabled) so we can check them for the ARM schema +// but also JSON/JSONC so we can check them for the ARM deployment and parameters schemas function shouldWatchDocument(textDocument: TextDocument): boolean { if ( textDocument.uri.scheme !== 'file' @@ -27,28 +43,31 @@ function shouldWatchDocument(textDocument: TextDocument): boolean { return false; } - if (textDocument.languageId === languageId) { + if (textDocument.languageId === armTemplateLanguageId) { return true; } - let enableAutoDetection = workspace.getConfiguration(configPrefix).get(configKeys.autoDetectJsonTemplates); - if (!enableAutoDetection) { - return false; - } - return isJsonOrJsoncLangId(textDocument); } +export function isAutoDetectArmEnabled(): boolean { + return !!workspace.getConfiguration(configPrefix).get(configKeys.autoDetectJsonTemplates); +} + export function mightBeDeploymentTemplate(textDocument: TextDocument): boolean { if (!shouldWatchDocument(textDocument)) { return false; } - if (textDocument.languageId === languageId) { + if (textDocument.languageId === armTemplateLanguageId) { return true; } if (isJsonOrJsoncLangId(textDocument)) { + if (!isAutoDetectArmEnabled()) { + return false; + } + let startOfDocument = textDocument.getText(new Range(new Position(0, 0), new Position(maxLinesToDetectSchemaIn - 1, 0))); // Do a quick dirty check if the first portion of the JSON contains a schema string that we're interested in @@ -58,3 +77,19 @@ export function mightBeDeploymentTemplate(textDocument: TextDocument): boolean { return false; } + +export function mightBeDeploymentParameters(textDocument: TextDocument): boolean { + if (!shouldWatchDocument(textDocument)) { + return false; + } + + if (isJsonOrJsoncLangId(textDocument)) { + let startOfDocument = textDocument.getText(new Range(new Position(0, 0), new Position(maxLinesToDetectSchemaIn - 1, 0))); + + // Do a quick dirty check if the first portion of the JSON contains a schema string that we're interested in + // (might not actually be in a $schema property, though) + return !!startOfDocument && containsParametersSchema(startOfDocument); + } + + return false; +} diff --git a/src/util/InitializeBeforeUse.ts b/src/util/InitializeBeforeUse.ts new file mode 100644 index 000000000..e53367828 --- /dev/null +++ b/src/util/InitializeBeforeUse.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from "../fixed_assert"; + +/** + * Represents a scalar value that must be initialized before its getValue is called + */ +export class InitializeBeforeUse { + private _value: { value: T; initialized: true } | { initialized: false } = { initialized: false }; + + public setValue(value: T): void { + this._value = { value: value, initialized: true }; + } + + public getValue(): T { + if (this._value.initialized) { + return this._value.value; + } else { + assert.fail("ExtensionVariables has not been fully initialized"); + } + } +} diff --git a/src/util/normalizePath.ts b/src/util/normalizePath.ts new file mode 100644 index 000000000..ddae52341 --- /dev/null +++ b/src/util/normalizePath.ts @@ -0,0 +1,18 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import * as path from 'path'; +import { Uri } from "vscode"; +import { isWin32 } from '../constants'; + +export function normalizePath(filePath: Uri | string): string { + const fsPath: string = typeof filePath === 'string' ? filePath : + filePath.fsPath; + let normalizedPath = path.normalize(fsPath); + if (isWin32) { + normalizedPath = normalizedPath.toLowerCase(); + } + + return normalizedPath; +} diff --git a/src/util/throwOnCancel.ts b/src/util/throwOnCancel.ts new file mode 100644 index 000000000..d9ac389dd --- /dev/null +++ b/src/util/throwOnCancel.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vscode'; +import { IActionContext, UserCancelledError } from 'vscode-azureextensionui'; +import { CancellationTokenSource } from 'vscode-jsonrpc'; + +export class Cancellation { + public static cantCancel: Cancellation = new Cancellation(new CancellationTokenSource().token); + + public constructor(public token: CancellationToken, public actionContext?: IActionContext) { + this.throwIfCancelled(); + } + + public throwIfCancelled(): void { + throwOnCancel(this.token, this.actionContext); + } +} + +export function throwOnCancel(token: CancellationToken, actionContext?: IActionContext): void { + if (token.isCancellationRequested) { + if (actionContext) { + actionContext.telemetry.properties.cancelStep = 'vscode'; + } + + throw new UserCancelledError(); + } +} diff --git a/src/util/toVsCodeCompletionItem.ts b/src/util/toVsCodeCompletionItem.ts new file mode 100644 index 000000000..0eacb4544 --- /dev/null +++ b/src/util/toVsCodeCompletionItem.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IActionContext } from 'vscode-azureextensionui'; +import * as Completion from '../Completion'; +import { DeploymentDocument } from '../DeploymentDocument'; +import { assertNever } from './assertNever'; +import { getVSCodeRangeFromSpan } from './vscodePosition'; + +interface ICompletionActivated { + completionKind: string; + snippetName: string; +} + +export function toVsCodeCompletionItem(deploymentFile: DeploymentDocument, item: Completion.Item): vscode.CompletionItem { + const insertRange: vscode.Range = getVSCodeRangeFromSpan(deploymentFile, item.insertSpan); + + const vscodeItem = new vscode.CompletionItem(item.label); + vscodeItem.range = insertRange; + vscodeItem.insertText = new vscode.SnippetString(item.insertText); + vscodeItem.detail = item.detail; + vscodeItem.documentation = item.documention; + + switch (item.kind) { + case Completion.CompletionKind.Function: + vscodeItem.kind = vscode.CompletionItemKind.Function; + break; + + case Completion.CompletionKind.Parameter: + case Completion.CompletionKind.Variable: + vscodeItem.kind = vscode.CompletionItemKind.Variable; + break; + + case Completion.CompletionKind.Property: + vscodeItem.kind = vscode.CompletionItemKind.Field; + break; + + case Completion.CompletionKind.Namespace: + vscodeItem.kind = vscode.CompletionItemKind.Unit; + break; + + case Completion.CompletionKind.PropertyValue: + vscodeItem.kind = vscode.CompletionItemKind.Property; + break; + + case Completion.CompletionKind.NewPropertyValue: + vscodeItem.kind = vscode.CompletionItemKind.Snippet; + break; + + default: + assertNever(item.kind); + } + + if (item.additionalEdits) { + vscodeItem.additionalTextEdits = item.additionalEdits.map( + e => new vscode.TextEdit( + getVSCodeRangeFromSpan(deploymentFile, e.span), + e.insertText + ) + ); + } + + // Add a command to let us know when activated so we can send telemetry + vscodeItem.command = { + command: "azurerm-vscode-tools.completion-activated", + title: "completion activated", // won't ever be shown to the user + arguments: [ + { + snippetName: item.snippetName, + completionKind: item.kind + } + ] + }; + + return vscodeItem; +} + +export function onCompletionActivated(actionContext: IActionContext, args: object): void { + const options = args ?? {}; + actionContext.telemetry.properties.snippetName = options.snippetName; + actionContext.telemetry.properties.completionKind = options.completionKind; +} diff --git a/src/util/vscodePosition.ts b/src/util/vscodePosition.ts index c9675e352..ad254a156 100644 --- a/src/util/vscodePosition.ts +++ b/src/util/vscodePosition.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { DeploymentTemplate } from '../DeploymentTemplate'; +import { DeploymentDocument } from '../DeploymentDocument'; import { assert } from "../fixed_assert"; import * as language from "../Language"; -export function getVSCodeRangeFromSpan(deploymentTemplate: DeploymentTemplate, span: language.Span): vscode.Range { +export function getVSCodeRangeFromSpan(deploymentDocument: DeploymentDocument, span: language.Span): vscode.Range { assert(span); - assert(deploymentTemplate); + assert(deploymentDocument); - const startPosition: language.Position = deploymentTemplate.getContextFromDocumentCharacterIndex(span.startIndex).documentPosition; + const startPosition: language.Position = deploymentDocument.getDocumentPosition(span.startIndex); const vscodeStartPosition = new vscode.Position(startPosition.line, startPosition.column); - const endPosition: language.Position = deploymentTemplate.getContextFromDocumentCharacterIndex(span.afterEndIndex).documentPosition; + const endPosition: language.Position = deploymentDocument.getDocumentPosition(span.afterEndIndex); const vscodeEndPosition = new vscode.Position(endPosition.line, endPosition.column); return new vscode.Range(vscodeStartPosition, vscodeEndPosition); diff --git a/src/visitors/FindReferencesVisitor.ts b/src/visitors/FindReferencesVisitor.ts index 9a373eda5..400c4f75c 100644 --- a/src/visitors/FindReferencesVisitor.ts +++ b/src/visitors/FindReferencesVisitor.ts @@ -2,8 +2,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ---------------------------------------------------------------------------- +import { Language } from "../../extension.bundle"; import { BuiltinFunctionMetadata, FunctionsMetadata } from "../AzureRMAssets"; import { templateKeys } from "../constants"; +import { DeploymentDocument } from "../DeploymentDocument"; import { assert } from '../fixed_assert'; import { DefinitionKind, INamedDefinition } from "../INamedDefinition"; import * as Reference from "../ReferenceList"; @@ -20,6 +22,7 @@ export class FindReferencesVisitor extends Visitor { private _lowerCasedFullName: string; constructor( + private readonly _document: DeploymentDocument, private readonly _definition: INamedDefinition, private readonly _functionsMetadata: FunctionsMetadata ) { @@ -39,6 +42,13 @@ export class FindReferencesVisitor extends Visitor { return this._references; } + private addReference(span: Language.Span): void { + this._references.add({ + document: this._document, + span: span + }); + } + // tslint:disable-next-line:cyclomatic-complexity public visitFunctionCall(tleFunction: FunctionCallValue): void { switch (this._definition.definitionKind) { @@ -46,7 +56,7 @@ export class FindReferencesVisitor extends Visitor { if (tleFunction.nameToken && tleFunction.name && tleFunction.namespace) { const userFunctionDefinition: UserFunctionDefinition | undefined = tleFunction.scope.getUserFunctionDefinition(tleFunction.namespace, tleFunction.name); if (userFunctionDefinition === this._definition) { - this._references.add(tleFunction.nameToken.span); + this.addReference(tleFunction.nameToken.span); } } break; @@ -55,7 +65,7 @@ export class FindReferencesVisitor extends Visitor { if (tleFunction.namespaceToken && tleFunction.namespace) { const userNamespaceDefinition: UserFunctionNamespaceDefinition | undefined = tleFunction.scope.getFunctionNamespaceDefinition(tleFunction.namespace); if (userNamespaceDefinition === this._definition) { - this._references.add(tleFunction.namespaceToken.span); + this.addReference(tleFunction.namespaceToken.span); } } break; @@ -66,7 +76,7 @@ export class FindReferencesVisitor extends Visitor { if (this._definition instanceof BuiltinFunctionMetadata) { // Metadata is not guaranteed to be the same each call, so compare name instead of definition if (metadata && metadata.lowerCaseName === this._lowerCasedFullName) { - this._references.add(tleFunction.nameToken.span); + this.addReference(tleFunction.nameToken.span); } } else { assert(false, "Expected reference definition to be BuiltinFunctionMetadata"); @@ -82,7 +92,7 @@ export class FindReferencesVisitor extends Visitor { const argName = arg.toString(); const paramDefinition = tleFunction.scope.getParameterDefinition(argName); if (paramDefinition === this._definition) { - this._references.add(arg.unquotedSpan); + this.addReference(arg.unquotedSpan); } } } @@ -97,23 +107,27 @@ export class FindReferencesVisitor extends Visitor { const argName = arg.toString(); const varDefinition = tleFunction.scope.getVariableDefinition(argName); if (varDefinition === this._definition) { - this._references.add(arg.unquotedSpan); + this.addReference(arg.unquotedSpan); } } } } break; + case DefinitionKind.ParameterValue: + // tslint:disable-next-line:no-suspicious-comment + // TODO: To implement + break; + default: assertNever(this._definition.definitionKind); - break; } super.visitFunctionCall(tleFunction); } - public static visit(tleValue: Value | undefined, definition: INamedDefinition, metadata: FunctionsMetadata): FindReferencesVisitor { - const visitor = new FindReferencesVisitor(definition, metadata); + public static visit(document: DeploymentDocument, tleValue: Value | undefined, definition: INamedDefinition, metadata: FunctionsMetadata): FindReferencesVisitor { + const visitor = new FindReferencesVisitor(document, definition, metadata); if (tleValue) { tleValue.accept(visitor); } diff --git a/test/Completion.test.ts b/test/Completion.test.ts index cd1fb1b94..244792ca5 100644 --- a/test/Completion.test.ts +++ b/test/Completion.test.ts @@ -8,12 +8,12 @@ import { Completion, Language } from "../extension.bundle"; suite("Completion", () => { suite("Item", () => { test("constructor(string, Span, string, string, Type)", () => { - const item: Completion.Item = new Completion.Item("a", "b", new Language.Span(1, 2), "c", "d", Completion.CompletionKind.Function); - assert.deepStrictEqual(item.description, "d"); + const item: Completion.Item = new Completion.Item("a", "b", new Language.Span(1, 2), Completion.CompletionKind.Function, "c", "d"); + assert.deepStrictEqual(item.documention, "d"); assert.deepStrictEqual(item.detail, "c"); assert.deepStrictEqual(item.insertSpan, new Language.Span(1, 2)); assert.deepStrictEqual(item.insertText, "b"); - assert.deepStrictEqual(item.name, "a"); + assert.deepStrictEqual(item.label, "a"); assert.deepStrictEqual(item.kind, Completion.CompletionKind.Function); }); }); diff --git a/test/DeploymentFileMapping.test.ts b/test/DeploymentFileMapping.test.ts new file mode 100644 index 000000000..4a8dcbc4f --- /dev/null +++ b/test/DeploymentFileMapping.test.ts @@ -0,0 +1,193 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length insecure-random +// tslint:disable:object-literal-key-quotes no-function-expression no-non-null-assertion align no-http-string + +import * as assert from 'assert'; +import * as path from 'path'; +import { Uri } from "vscode"; +import { DeploymentFileMapping, isWin32, normalizePath } from "../extension.bundle"; +import { TestConfiguration } from "./support/TestConfiguration"; +import { testOnWin32 } from './support/testOnPlatform'; + +suite("DeploymentFileMapping", () => { + const root = isWin32 ? "c:\\" : "/"; + const param1 = Uri.file(isWin32 ? "c:\\temp\\template1.params.json" : "/temp/template1.params.json"); + const param1variation = Uri.file(isWin32 ? "c:\\temp\\abc\\..\\template1.params.json" : "/temp/abc/../template1.params.json"); + + const param1subfolder = Uri.file(isWin32 ? "c:\\temp\\sub\\template1.params.json" : "/temp/sub/template1.params.json"); + const param1parentfolder = Uri.file(isWin32 ? "c:\\template1.params.json" : "/template1.params.json"); + + const template1 = Uri.file(isWin32 ? "c:\\temp\\template1.json" : "/temp/template1.json"); + const template1variation = Uri.file(isWin32 ? "c:\\temp\\abc\\..\\template1.json" : "/temp/abc/../template1.json"); + + test("Update/get", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = template1; + const p = param1; + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(t)?.fsPath, p.fsPath); + assert.equal(mapping.getTemplateFile(p)?.fsPath, t.fsPath); + }); + + test("Update/get - paths normalized", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = template1variation; + const p = param1variation; + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(t)?.fsPath, path.resolve(p.fsPath)); + assert.equal(mapping.getTemplateFile(p)?.fsPath, path.resolve(t.fsPath)); + }); + + test("Update/get - param in subfolder", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = template1; + const p = param1subfolder; + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(t)?.fsPath, p.fsPath); + assert.equal(mapping.getTemplateFile(p)?.fsPath, t.fsPath); + }); + + test("Update/get - param in parent folder", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = template1; + const p = param1parentfolder; + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(t)?.fsPath, p.fsPath); + assert.equal(mapping.getTemplateFile(p)?.fsPath, t.fsPath); + }); + + testOnWin32("Update/get - param on different drive", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = Uri.file("c:\\temp\\template1.params.json"); + const p = Uri.file("d:\\temp\\template1.params.json"); + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(t)?.fsPath, p.fsPath); + assert.equal(mapping.getTemplateFile(p)?.fsPath, t.fsPath); + }); + + test("Param paths are stored in settings relative to template folder", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = template1; + const p = param1subfolder; + + await mapping.mapParameterFile(t, p); + + const parameterFiles = <{ [key: string]: string }>testConfig.get("parameterFiles"); + const pStoredPath = parameterFiles[t.fsPath]; + assert(!path.isAbsolute(pStoredPath)); + }); + + testOnWin32("Case-insensitive on Windows", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t = Uri.file("C:\\TEMP\\Template1.json"); + const p = Uri.file("C:\\TEMP\\Template1.Params.json"); + + await mapping.mapParameterFile(t, p); + + assert.equal(mapping.getParameterFile(Uri.file("c:\\temp\\tEMPLATE1.jSON"))?.fsPath, p.fsPath.toLocaleLowerCase()); + assert.equal(mapping.getTemplateFile(Uri.file("c:\\temP\\TemPLATE1.pARAMS.jSOn"))?.fsPath, t.fsPath.toLowerCase()); + }); + + testOnWin32("Case-insensitive on Windows - last entry wins", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const t1 = Uri.file("C:\\TEMP\\Template1.json"); + const t2 = Uri.file("C:\\TEMP\\TEMPLATE1.json"); + const t3 = Uri.file("C:\\TEMP\\tempLATE1.json"); + const p1 = Uri.file("C:\\TEMP\\Template1.Params.json"); + const p2 = Uri.file("C:\\TEMP\\TEMPLATE1.PARAMS.json"); + const p3 = Uri.file("C:\\TEMP\\tempLATE1.paRAMS.json"); + + const obj: { [key: string]: unknown } = {}; + testConfig.Test_globalValues.set("parameterFiles", obj); + obj[t1.fsPath] = p1.fsPath; + obj[t2.fsPath] = p2.fsPath; + obj[t3.fsPath] = p3.fsPath; + + // Look-up on any template version returns p2 + const normalizedTemplate = normalizePath(t1.fsPath); + assert(normalizedTemplate === normalizePath(t2.fsPath) && normalizedTemplate === normalizePath(t3.fsPath)); + assert.equal(mapping.getParameterFile(t1)?.fsPath, p3.fsPath); + assert.equal(mapping.getParameterFile(t2)?.fsPath, p3.fsPath); + assert.equal(mapping.getParameterFile(t3)?.fsPath, p3.fsPath); + + // Look-up on any parameter version returns same path + assert.equal(mapping.getTemplateFile(p1)?.fsPath, t3.fsPath); + assert.equal(mapping.getTemplateFile(p2)?.fsPath, t3.fsPath); + assert.equal(mapping.getTemplateFile(p3)?.fsPath, t3.fsPath); + }); + + test("Bad settings 1", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + + assert.equal(mapping.getParameterFile(template1), undefined); + }); + + test("Bad settings 2", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + const obj: { [key: string]: unknown } = {}; + testConfig.Test_globalValues.set("parameterFiles", obj); + obj[undefined] = "foo"; + obj[undefined] = "foo"; + obj[""] = "foo"; + obj.goo = "foo"; + obj["temp/relative/foo.json"] = "foo"; + obj["."] = "foo"; + + obj[`${root}t1.json`] = undefined; + obj[`${root}t2.json`] = ""; + obj[`${root}t3.json`] = 1; + obj[`${root}t4.json`] = {}; + obj[`${root}t5.json`] = "."; + obj[`${root}t6.json`] = "/"; + obj[`${root}good1.json`] = "a."; + + // getParameterFile + assert.equal(mapping.getParameterFile(Uri.file("")), undefined); + assert.equal(mapping.getParameterFile(Uri.file("foo")), undefined); + assert.equal(mapping.getParameterFile(Uri.file("temp/relative/foo.json")), undefined); + assert.equal(mapping.getParameterFile(Uri.file(`${root}t1.json`)), undefined); + assert.equal(mapping.getParameterFile(Uri.file(`${root}t3.json`)), undefined); + assert.equal(mapping.getParameterFile(Uri.file(`${root}t4.json`)), undefined); + assert.equal(mapping.getParameterFile(Uri.file(`${root}good1.json`))?.fsPath, `${root}a.`); + + // getTemplateFile + assert.equal(mapping.getTemplateFile(Uri.file("")), undefined); + assert.equal(mapping.getTemplateFile(Uri.file("foo.params.json")), undefined); + }); + + test("Remove mapping", async () => { + const testConfig = new TestConfiguration(); + const mapping = new DeploymentFileMapping(testConfig); + + await mapping.mapParameterFile(template1, param1); + assert.equal(mapping.getParameterFile(template1)?.fsPath, param1.fsPath); + assert.equal(mapping.getTemplateFile(param1)?.fsPath, template1.fsPath); + + await mapping.mapParameterFile(template1, undefined); + assert.equal(mapping.getParameterFile(template1), undefined); + assert.equal(mapping.getTemplateFile(param1), undefined); + }); +}); diff --git a/test/DeploymentParameters.test.ts b/test/DeploymentParameters.test.ts new file mode 100644 index 000000000..3c2ec7d6b --- /dev/null +++ b/test/DeploymentParameters.test.ts @@ -0,0 +1,85 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length insecure-random +// tslint:disable:object-literal-key-quotes no-function-expression no-non-null-assertion align no-http-string + +import * as assert from "assert"; +import { Uri } from "vscode"; +import { DeploymentParameters, ParameterValueDefinition } from "../extension.bundle"; + +const fakeId = Uri.file("https://fake-id"); + +suite("DeploymentParameters", () => { + + suite("constructor(string)", () => { + test("Empty stringValue", () => { + const dt = new DeploymentParameters("", fakeId); + assert.deepStrictEqual("", dt.documentText); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); + assert.deepStrictEqual([], dt.parameterValues); + }); + + test("Non-JSON stringValue", () => { + const dt = new DeploymentParameters("I'm not a JSON file", fakeId); + assert.deepStrictEqual("I'm not a JSON file", dt.documentText); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); + assert.deepStrictEqual([], dt.parameterValues); + }); + + test("JSON stringValue with number parameters definition", () => { + const dt = new DeploymentParameters("{ 'parameters': 21 }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); + assert.deepStrictEqual([], dt.parameterValues); + }); + + test("JSON stringValue with empty object parameters definition", () => { + const dt = new DeploymentParameters("{ 'parameters': {} }", fakeId); + assert.deepStrictEqual("{ 'parameters': {} }", dt.documentText); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); + assert.deepStrictEqual([], dt.parameterValues); + }); + + test("JSON stringValue with one parameter value", () => { + const dt = new DeploymentParameters("{ 'parameters': { 'num': { 'value': 1 } } }", fakeId); + const parameterValues: ParameterValueDefinition[] = dt.parameterValues; + assert(parameterValues); + assert.deepStrictEqual(parameterValues.length, 1); + const pd0: ParameterValueDefinition = parameterValues[0]; + assert(pd0); + assert.deepStrictEqual(pd0.nameValue.toString(), "num"); + assert.deepStrictEqual(pd0.value?.toFriendlyString(), "1"); + }); + + test("JSON stringValue with one parameter definition with null value", () => { + const dt = new DeploymentParameters("{ 'parameters': { 'num': { 'value': null } } }", fakeId); + const parameterValues: ParameterValueDefinition[] = dt.parameterValues; + assert(parameterValues); + assert.deepStrictEqual(parameterValues.length, 1); + const pd0: ParameterValueDefinition = parameterValues[0]; + assert(pd0); + assert.deepStrictEqual(pd0.value?.toFriendlyString(), "null"); + }); + + test("JSON stringValue with one parameter definition with no value", () => { + const dt = new DeploymentParameters("{ 'parameters': { 'num': { } } }", fakeId); + const parameterValues: ParameterValueDefinition[] = dt.parameterValues; + assert(parameterValues); + assert.deepStrictEqual(parameterValues.length, 1); + const pd0: ParameterValueDefinition = parameterValues[0]; + assert(pd0); + assert.deepStrictEqual(pd0.value, undefined); + }); + + test("JSON stringValue with one parameter definition defined as a string", () => { + const dt = new DeploymentParameters("{ 'parameters': { 'num': 'whoops' } } }", fakeId); + const parameterValues: ParameterValueDefinition[] = dt.parameterValues; + assert(parameterValues); + assert.deepStrictEqual(parameterValues.length, 1); + const pd0: ParameterValueDefinition = parameterValues[0]; + assert(pd0); + assert.deepStrictEqual(pd0.value, undefined); + }); + }); +}); diff --git a/test/DeploymentTemplate.test.ts b/test/DeploymentTemplate.test.ts index e95510de7..c923d656b 100644 --- a/test/DeploymentTemplate.test.ts +++ b/test/DeploymentTemplate.test.ts @@ -8,6 +8,7 @@ import * as assert from "assert"; import { randomBytes } from "crypto"; import { ISuiteCallbackContext, ITestCallbackContext } from "mocha"; +import { Uri } from "vscode"; import { DefinitionKind, DeploymentTemplate, Histogram, INamedDefinition, IncorrectArgumentsCountIssue, IParameterDefinition, IVariableDefinition, Json, Language, ReferenceInVariableDefinitionsVisitor, ReferenceList, TemplateScope, UnrecognizedUserFunctionIssue, UnrecognizedUserNamespaceIssue } from "../extension.bundle"; import { IDeploymentTemplate, sources, testDiagnostics } from "./support/diagnostics"; import { parseTemplate } from "./support/parseTemplate"; @@ -18,6 +19,8 @@ import { DISABLE_SLOW_TESTS } from "./testConstants"; const IssueKind = Language.IssueKind; const tleSyntax = IssueKind.tleSyntax; +const fakeId = Uri.file("https://fake-id"); + suite("DeploymentTemplate", () => { function findReferences(dt: DeploymentTemplate, definitionKind: DefinitionKind, definitionName: string, scope: TemplateScope): ReferenceList { @@ -46,52 +49,52 @@ suite("DeploymentTemplate", () => { return new ReferenceList(definitionKind, []); } - return dt.findReferences(definition!); + return dt.findReferencesToDefinition(definition!); } suite("constructor(string)", () => { test("Null stringValue", () => { // tslint:disable-next-line:no-any - assert.throws(() => { new DeploymentTemplate(undefined, "id"); }); + assert.throws(() => { new DeploymentTemplate(undefined, fakeId); }); }); test("Undefined stringValue", () => { // tslint:disable-next-line:no-any - assert.throws(() => { new DeploymentTemplate(undefined, "id"); }); + assert.throws(() => { new DeploymentTemplate(undefined, fakeId); }); }); test("Empty stringValue", () => { - const dt = new DeploymentTemplate("", "id"); + const dt = new DeploymentTemplate("", fakeId); assert.deepStrictEqual("", dt.documentText); - assert.deepStrictEqual("id", dt.documentId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual([], dt.topLevelScope.parameterDefinitions); }); test("Non-JSON stringValue", () => { - const dt = new DeploymentTemplate("I'm not a JSON file", "id"); + const dt = new DeploymentTemplate("I'm not a JSON file", fakeId); assert.deepStrictEqual("I'm not a JSON file", dt.documentText); - assert.deepStrictEqual("id", dt.documentId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual([], dt.topLevelScope.parameterDefinitions); }); test("JSON stringValue with number parameters definition", () => { - const dt = new DeploymentTemplate("{ 'parameters': 21 }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': 21 }", fakeId); assert.deepStrictEqual("{ 'parameters': 21 }", dt.documentText); - assert.deepStrictEqual("id", dt.documentId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual([], dt.topLevelScope.parameterDefinitions); }); test("JSON stringValue with empty object parameters definition", () => { - const dt = new DeploymentTemplate("{ 'parameters': {} }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': {} }", fakeId); assert.deepStrictEqual("{ 'parameters': {} }", dt.documentText); - assert.deepStrictEqual("id", dt.documentId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual([], dt.topLevelScope.parameterDefinitions); }); test("JSON stringValue with one parameter definition", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number' } } }", fakeId); assert.deepStrictEqual("{ 'parameters': { 'num': { 'type': 'number' } } }", dt.documentText); - assert.deepStrictEqual("id", dt.documentId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -103,8 +106,8 @@ suite("DeploymentTemplate", () => { }); test("JSON stringValue with one parameter definition with undefined description", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': null } } } }", "id"); - assert.deepStrictEqual("id", dt.documentId); + const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': null } } } }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -116,8 +119,8 @@ suite("DeploymentTemplate", () => { }); test("JSON stringValue with one parameter definition with empty description", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': '' } } } }", "id"); - assert.deepStrictEqual("id", dt.documentId); + const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': '' } } } }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -129,8 +132,8 @@ suite("DeploymentTemplate", () => { }); test("JSON stringValue with one parameter definition with non-empty description", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': 'num description' } } } }", "id"); - assert.deepStrictEqual("id", dt.documentId); + const dt = new DeploymentTemplate("{ 'parameters': { 'num': { 'type': 'number', 'metadata': { 'description': 'num description' } } } }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -142,15 +145,15 @@ suite("DeploymentTemplate", () => { }); test("JSON stringValue with number variable definitions", () => { - const dt = new DeploymentTemplate("{ 'variables': 12 }", "id"); - assert.deepStrictEqual("id", dt.documentId); + const dt = new DeploymentTemplate("{ 'variables': 12 }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual("{ 'variables': 12 }", dt.documentText); assert.deepStrictEqual([], dt.topLevelScope.variableDefinitions); }); test("JSON stringValue with one variable definition", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'variables': { 'a': 'A' } }", "id"); - assert.deepStrictEqual(dt.documentId, "id"); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'variables': { 'a': 'A' } }", fakeId); + assert.deepStrictEqual(dt.documentId, fakeId); assert.deepStrictEqual(dt.documentText, "{ 'variables': { 'a': 'A' } }"); assert.deepStrictEqual(dt.topLevelScope.variableDefinitions.length, 1); assert.deepStrictEqual(dt.topLevelScope.variableDefinitions[0].nameValue.toString(), "a"); @@ -162,8 +165,8 @@ suite("DeploymentTemplate", () => { }); test("JSON stringValue with two variable definitions", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'a': 'A', 'b': 2 } }", "id"); - assert.deepStrictEqual("id", dt.documentId); + const dt = new DeploymentTemplate("{ 'variables': { 'a': 'A', 'b': 2 } }", fakeId); + assert.deepStrictEqual(fakeId.fsPath, dt.documentId.fsPath); assert.deepStrictEqual("{ 'variables': { 'a': 'A', 'b': 2 } }", dt.documentText); assert.deepStrictEqual(dt.topLevelScope.variableDefinitions.length, 2); @@ -182,62 +185,62 @@ suite("DeploymentTemplate", () => { suite("errors", () => { test("with empty deployment template", () => { - const dt = new DeploymentTemplate("", "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate("", fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, []); }); }); test("with empty object deployment template", () => { - const dt = new DeploymentTemplate("{}", "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate("{}", fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, []); }); }); test("with one property deployment template", () => { - const dt = new DeploymentTemplate("{ 'name': 'value' }", "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate("{ 'name': 'value' }", fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, []); }); }); test("with one TLE parse error deployment template", () => { - const dt = new DeploymentTemplate("{ 'name': '[concat()' }", "id"); + const dt = new DeploymentTemplate("{ 'name': '[concat()' }", fakeId); const expectedErrors = [ new Language.Issue(new Language.Span(20, 1), "Expected a right square bracket (']').", tleSyntax) ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); test("with one undefined parameter error deployment template", () => { - const dt = new DeploymentTemplate("{ 'name': '[parameters(\"test\")]' }", "id"); + const dt = new DeploymentTemplate("{ 'name': '[parameters(\"test\")]' }", fakeId); const expectedErrors = [ new Language.Issue(new Language.Span(23, 6), "Undefined parameter reference: \"test\"", IssueKind.undefinedParam) ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); test("with one undefined variable error deployment template", () => { - const dt = new DeploymentTemplate("{ 'name': '[variables(\"test\")]' }", "id"); + const dt = new DeploymentTemplate("{ 'name': '[variables(\"test\")]' }", fakeId); const expectedErrors = [ new Language.Issue(new Language.Span(22, 6), "Undefined variable reference: \"test\"", IssueKind.undefinedVar) ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); test("with one unrecognized user namespace error deployment template", () => { - const dt = new DeploymentTemplate("{ \"name\": \"[namespace.blah('test')]\" }", "id"); + const dt = new DeploymentTemplate("{ \"name\": \"[namespace.blah('test')]\" }", fakeId); const expectedErrors = [ new UnrecognizedUserNamespaceIssue(new Language.Span(12, 9), "namespace") ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); @@ -266,11 +269,11 @@ suite("DeploymentTemplate", () => { } ] }), - "id"); + fakeId); const expectedErrors = [ new UnrecognizedUserFunctionIssue(new Language.Span(22, 4), "contoso", "blah") ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); @@ -295,10 +298,10 @@ suite("DeploymentTemplate", () => { } ] }`, - "id"); + fakeId); const expectedErrors: string[] = [ ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); @@ -341,11 +344,11 @@ suite("DeploymentTemplate", () => { } ] }), - "id"); + fakeId); const expectedErrors = [ new UnrecognizedUserFunctionIssue(new Language.Span(22, 9), "contoso", "reference") ]; - return dt.errorsPromise.then((errors: Language.Issue[]) => { + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual(errors, expectedErrors); }); }); @@ -372,17 +375,17 @@ suite("DeploymentTemplate", () => { } ] }), - "id"); + fakeId); const expectedErrors = [ new Language.Issue(new Language.Span(243, 6), "User functions cannot reference variables", IssueKind.varInUdf) ]; - const errors: Language.Issue[] = await dt.errorsPromise; + const errors: Language.Issue[] = await dt.getErrors(undefined); assert.deepStrictEqual(errors, expectedErrors); }); test("with reference() call in variable definition", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(24, 9), "reference() cannot be invoked inside of a variable definition.", IssueKind.referenceInVar)] @@ -426,8 +429,8 @@ suite("DeploymentTemplate", () => { }); test("with reference() call inside a different expression in a variable definition", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[concat(reference('test'))]" } }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": "[concat(reference('test'))]" } }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(31, 9), "reference() cannot be invoked inside of a variable definition.", IssueKind.referenceInVar)]); @@ -435,8 +438,8 @@ suite("DeploymentTemplate", () => { }); test("with unnamed property access on variable reference", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables('a').]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables('a').]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(50, 1), "Expected a literal value.", tleSyntax)]); @@ -444,8 +447,8 @@ suite("DeploymentTemplate", () => { }); test("with property access on variable reference without variable name", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables().b]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables().b]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new IncorrectArgumentsCountIssue(new Language.Span(35, 11), "The function 'variables' takes 1 argument.", "variables", 0, 1, 1)]); @@ -453,8 +456,8 @@ suite("DeploymentTemplate", () => { }); test("with property access on string variable reference", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "A" }, "z": "[variables('a').b]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": "A" }, "z": "[variables('a').b]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(51, 1), `Property "b" is not a defined property of "variables('a')".`, IssueKind.undefinedVarProp)]); @@ -462,8 +465,8 @@ suite("DeploymentTemplate", () => { }); test("with undefined variable reference child property", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables('a').b]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": {} }, "z": "[variables('a').b]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(50, 1), `Property "b" is not a defined property of "variables('a')".`, IssueKind.undefinedVarProp)]); @@ -471,8 +474,8 @@ suite("DeploymentTemplate", () => { }); test("with undefined variable reference grandchild property", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": { "b": {} } }, "z": "[variables('a').b.c]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": { "b": {} } }, "z": "[variables('a').b.c]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(61, 1), `Property "c" is not a defined property of "variables('a').b".`, IssueKind.undefinedVarProp)]); @@ -480,8 +483,8 @@ suite("DeploymentTemplate", () => { }); test("with undefined variable reference child and grandchild properties", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": { "d": {} } }, "z": "[variables('a').b.c]" }`, "id"); - return dt.errorsPromise.then((errors: Language.Issue[]) => { + const dt = new DeploymentTemplate(`{ "variables": { "a": { "d": {} } }, "z": "[variables('a').b.c]" }`, fakeId); + return dt.getErrors(undefined).then((errors: Language.Issue[]) => { assert.deepStrictEqual( errors, [new Language.Issue(new Language.Span(59, 1), `Property "b" is not a defined property of "variables('a')".`, IssueKind.undefinedVarProp)]); @@ -491,56 +494,56 @@ suite("DeploymentTemplate", () => { suite("warnings", () => { test("with unused parameter", () => { - const dt = new DeploymentTemplate(`{ "parameters": { "a": {} } }`, "id"); + const dt = new DeploymentTemplate(`{ "parameters": { "a": {} } }`, fakeId); assert.deepStrictEqual( - dt.warnings, + dt.getWarnings(), [new Language.Issue(new Language.Span(18, 3), "The parameter 'a' is never used.", IssueKind.unusedParam)]); }); test("with no unused parameters", async () => { - const dt = new DeploymentTemplate(`{ "parameters": { "a": {} }, "b": "[parameters('a')] }`, "id"); - assert.deepStrictEqual(dt.warnings, []); - assert.deepStrictEqual(dt.warnings, []); + const dt = new DeploymentTemplate(`{ "parameters": { "a": {} }, "b": "[parameters('a')] }`, fakeId); + assert.deepStrictEqual(dt.getWarnings(), []); + assert.deepStrictEqual(dt.getWarnings(), []); }); test("with unused variable", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "A" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "A" } }`, fakeId); assert.deepStrictEqual( - dt.warnings, + dt.getWarnings(), [new Language.Issue(new Language.Span(17, 3), "The variable 'a' is never used.", IssueKind.unusedVar)]); }); test("with no unused variables", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "A" }, "b": "[variables('a')] }`, "id"); - assert.deepStrictEqual(dt.warnings, []); - assert.deepStrictEqual(dt.warnings, []); + const dt = new DeploymentTemplate(`{ "variables": { "a": "A" }, "b": "[variables('a')] }`, fakeId); + assert.deepStrictEqual(dt.getWarnings(), []); + assert.deepStrictEqual(dt.getWarnings(), []); }); }); suite("get functionCounts()", () => { test("with empty deployment template", () => { - const dt = new DeploymentTemplate("", "id"); + const dt = new DeploymentTemplate("", fakeId); const expectedHistogram = new Histogram(); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); }); test("with empty object deployment template", () => { - const dt = new DeploymentTemplate("{}", "id"); + const dt = new DeploymentTemplate("{}", fakeId); const expectedHistogram = new Histogram(); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); }); test("with one property object deployment template", () => { - const dt = new DeploymentTemplate("{ 'name': 'value' }", "id"); + const dt = new DeploymentTemplate("{ 'name': 'value' }", fakeId); const expectedHistogram = new Histogram(); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); assert.deepStrictEqual(expectedHistogram, dt.getFunctionCounts()); }); test("with one TLE function used multiple times in deployment template", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'name': '[concat()]', 'name2': '[concat(1, 2)]', 'name3': '[concat(2, 3)]' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'name': '[concat()]', 'name2': '[concat(1, 2)]', 'name3': '[concat(2, 3)]' } }", fakeId); const expectedHistogram = new Histogram(); expectedHistogram.add("concat"); expectedHistogram.add("concat"); @@ -553,7 +556,7 @@ suite("DeploymentTemplate", () => { }); test("with two TLE functions in different TLEs deployment template", () => { - const dt = new DeploymentTemplate(`{ "name": "[concat()]", "height": "[add()]" }`, "id"); + const dt = new DeploymentTemplate(`{ "name": "[concat()]", "height": "[add()]" }`, fakeId); const expectedHistogram = new Histogram(); expectedHistogram.add("concat"); expectedHistogram.add("concat(0)"); @@ -564,7 +567,7 @@ suite("DeploymentTemplate", () => { }); test("with the same string repeated in multiple places (each use should get counted once, even though the strings are the exact same and may be cached)", () => { - const dt = new DeploymentTemplate("{ 'name': '[concat()]', 'height': '[concat()]', 'width': \"[concat()]\" }", "id"); + const dt = new DeploymentTemplate("{ 'name': '[concat()]', 'height': '[concat()]', 'width': \"[concat()]\" }", fakeId); assert.deepStrictEqual(3, dt.getFunctionCounts().getCount("concat(0)")); assert.deepStrictEqual(3, dt.getFunctionCounts().getCount("concat")); }); @@ -572,13 +575,13 @@ suite("DeploymentTemplate", () => { suite("get jsonParseResult()", () => { test("with empty deployment template", () => { - const dt = new DeploymentTemplate("", "id"); + const dt = new DeploymentTemplate("", fakeId); assert(dt.jsonParseResult); assert.equal(0, dt.jsonParseResult.tokenCount); }); test("with empty object deployment template", () => { - const dt = new DeploymentTemplate("{}", "id"); + const dt = new DeploymentTemplate("{}", fakeId); assert(dt.jsonParseResult); assert.equal(2, dt.jsonParseResult.tokenCount); }); @@ -599,7 +602,7 @@ suite("DeploymentTemplate", () => { } } }`, - "id" + fakeId ); assert.equal(dt.schemaUri, "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"); @@ -609,32 +612,32 @@ suite("DeploymentTemplate", () => { suite("get parameterDefinitions()", () => { test("with no parameters property", () => { - const dt = new DeploymentTemplate("{}", "id"); + const dt = new DeploymentTemplate("{}", fakeId); assert.deepStrictEqual(dt.topLevelScope.parameterDefinitions, []); }); test("with undefined parameters property", () => { - const dt = new DeploymentTemplate("{ 'parameters': undefined }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': undefined }", fakeId); assert.deepStrictEqual(dt.topLevelScope.parameterDefinitions, []); }); test("with string parameters property", () => { - const dt = new DeploymentTemplate("{ 'parameters': 'hello' }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': 'hello' }", fakeId); assert.deepStrictEqual(dt.topLevelScope.parameterDefinitions, []); }); test("with number parameters property", () => { - const dt = new DeploymentTemplate("{ 'parameters': 1 }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': 1 }", fakeId); assert.deepStrictEqual(dt.topLevelScope.parameterDefinitions, []); }); test("with empty object parameters property", () => { - const dt = new DeploymentTemplate("{ 'parameters': {} }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': {} }", fakeId); assert.deepStrictEqual(dt.topLevelScope.parameterDefinitions, []); }); test("with empty object parameter", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'a': {} } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'a': {} } }", fakeId); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -646,7 +649,7 @@ suite("DeploymentTemplate", () => { }); test("with parameter with metadata but no description", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'a': { 'metadata': {} } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'a': { 'metadata': {} } } }", fakeId); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -658,7 +661,7 @@ suite("DeploymentTemplate", () => { }); test("with parameter with metadata and description", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'a': { 'metadata': { 'description': 'b' } } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'a': { 'metadata': { 'description': 'b' } } } }", fakeId); const parameterDefinitions: IParameterDefinition[] = dt.topLevelScope.parameterDefinitions; assert(parameterDefinitions); assert.deepStrictEqual(parameterDefinitions.length, 1); @@ -672,44 +675,44 @@ suite("DeploymentTemplate", () => { suite("getParameterDefinition(string)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.getParameterDefinition(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.getParameterDefinition(undefined); }); }); test("with empty", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); assert.throws(() => { dt.topLevelScope.getParameterDefinition(""); }); }); test("with no parameters definition", () => { - const dt = new DeploymentTemplate("{}", "id"); + const dt = new DeploymentTemplate("{}", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getParameterDefinition("spam")); }); test("with unquoted non-match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getParameterDefinition("spam")); }); test("with one-sided-quote non-match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getParameterDefinition("'spam")); }); test("with quoted non-match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getParameterDefinition("'spam'")); }); test("with unquoted match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const apples: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("apples"); if (!apples) { throw new Error("failed"); } assert.deepStrictEqual(apples.nameValue.toString(), "apples"); @@ -720,7 +723,7 @@ suite("DeploymentTemplate", () => { }); test("with one-sided-quote match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const apples: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("'apples"); if (!apples) { throw new Error("failed"); } assert.deepStrictEqual(apples.nameValue.toString(), "apples"); @@ -729,7 +732,7 @@ suite("DeploymentTemplate", () => { }); test("with quoted match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const apples: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("'apples'"); if (!apples) { throw new Error("failed"); } assert.deepStrictEqual(apples.nameValue.toString(), "apples"); @@ -738,7 +741,7 @@ suite("DeploymentTemplate", () => { }); test("with case insensitive match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const apples: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("'APPLES'"); if (!apples) { throw new Error("failed"); } assert.deepStrictEqual(apples.nameValue.toString(), "apples"); @@ -756,7 +759,7 @@ suite("DeploymentTemplate", () => { 'Apples': { 'type': 'securestring' } } }), - "id"); + fakeId); // Should always match the last one defined when multiple have the same name const APPLES: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("'APPLES'"); @@ -771,7 +774,7 @@ suite("DeploymentTemplate", () => { // CONSIDER: Does JavaScript support this? It's low priority // test("with case insensitive match, Unicode", () => { // // Should always match the last one defined when multiple have the same name - // const dt = new DeploymentTemplate("{ 'parameters': { 'Strasse': { 'type': 'string' }, 'Straße': { 'type': 'integer' } } }", "id"); + // const dt = new DeploymentTemplate("{ 'parameters': { 'Strasse': { 'type': 'string' }, 'Straße': { 'type': 'integer' } } }",fakeId); // const strasse: IParameterDefinition | undefined = dt.topLevelScope.getParameterDefinition("'Strasse'"); // if (!strasse) { throw new Error("failed"); } // assert.deepStrictEqual(strasse.nameValue.toString(), "Straße"); @@ -788,19 +791,19 @@ suite("DeploymentTemplate", () => { suite("findParameterDefinitionsWithPrefix(string)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.findParameterDefinitionsWithPrefix(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.findParameterDefinitionsWithPrefix(undefined); }); }); test("with empty", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const matches: IParameterDefinition[] = dt.topLevelScope.findParameterDefinitionsWithPrefix(""); assert(matches); @@ -820,7 +823,7 @@ suite("DeploymentTemplate", () => { }); test("with prefix of one of the parameters", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const matches: IParameterDefinition[] = dt.topLevelScope.findParameterDefinitionsWithPrefix("ap"); assert(matches); @@ -834,12 +837,12 @@ suite("DeploymentTemplate", () => { }); test("with prefix of none of the parameters", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); assert.deepStrictEqual(dt.topLevelScope.findParameterDefinitionsWithPrefix("ca"), []); }); test("with case insensitive match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'bananas': { 'type': 'integer' } } }", fakeId); const matches: IParameterDefinition[] = dt.topLevelScope.findParameterDefinitionsWithPrefix("APP"); assert(matches); @@ -853,7 +856,7 @@ suite("DeploymentTemplate", () => { }); test("with case sensitive and insensitive match", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'APPLES': { 'type': 'integer' } } }", "id"); + const dt = new DeploymentTemplate("{ 'parameters': { 'apples': { 'type': 'string' }, 'APPLES': { 'type': 'integer' } } }", fakeId); const matches: IParameterDefinition[] = dt.topLevelScope.findParameterDefinitionsWithPrefix("APP"); assert(matches); @@ -875,44 +878,44 @@ suite("DeploymentTemplate", () => { suite("getVariableDefinition(string)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.getVariableDefinition(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.getVariableDefinition(undefined); }); }); test("with empty", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); assert.throws(() => { dt.topLevelScope.getVariableDefinition(""); }); }); test("with no variables definition", () => { - const dt = new DeploymentTemplate("{}", "id"); + const dt = new DeploymentTemplate("{}", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getVariableDefinition("spam")); }); test("with unquoted non-match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getVariableDefinition("spam")); }); test("with one-sided-quote non-match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getVariableDefinition("'spam")); }); test("with quoted non-match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); assert.deepStrictEqual(undefined, dt.topLevelScope.getVariableDefinition("'spam'")); }); test("with unquoted match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); const apples: IVariableDefinition | undefined = dt.topLevelScope.getVariableDefinition("apples"); if (!apples) { throw new Error("failed"); } @@ -925,7 +928,7 @@ suite("DeploymentTemplate", () => { }); test("with one-sided-quote match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); const apples: IVariableDefinition | undefined = dt.topLevelScope.getVariableDefinition("'apples"); if (!apples) { throw new Error("failed"); } @@ -938,7 +941,7 @@ suite("DeploymentTemplate", () => { }); test("with quoted match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); const apples: IVariableDefinition | undefined = dt.topLevelScope.getVariableDefinition("'apples'"); if (!apples) { throw new Error("failed"); } @@ -951,7 +954,7 @@ suite("DeploymentTemplate", () => { }); test("with case insensitive match", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'bananas': 'good' } }", fakeId); const apples: IVariableDefinition | undefined = dt.topLevelScope.getVariableDefinition("'APPLES"); if (!apples) { throw new Error("failed"); } @@ -964,7 +967,7 @@ suite("DeploymentTemplate", () => { }); test("with multiple case insensitive matches", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'APPLES': 'good' } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'yum', 'APPLES': 'good' } }", fakeId); // Should always find the last definition, because that's what Azure does const APPLES: IVariableDefinition | undefined = dt.topLevelScope.getVariableDefinition("'APPLES'"); @@ -987,19 +990,19 @@ suite("DeploymentTemplate", () => { suite("findVariableDefinitionsWithPrefix(string)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.findVariableDefinitionsWithPrefix(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", fakeId); // tslint:disable-next-line:no-any assert.throws(() => { dt.topLevelScope.findVariableDefinitionsWithPrefix(undefined); }); }); test("with empty", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", fakeId); const definitions: IVariableDefinition[] = dt.topLevelScope.findVariableDefinitionsWithPrefix(""); assert.deepStrictEqual(definitions.length, 2); @@ -1018,7 +1021,7 @@ suite("DeploymentTemplate", () => { }); test("with prefix of one of the variables", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", fakeId); const definitions: IVariableDefinition[] = dt.topLevelScope.findVariableDefinitionsWithPrefix("ap"); assert.deepStrictEqual(definitions.length, 1); @@ -1032,15 +1035,15 @@ suite("DeploymentTemplate", () => { }); test("with prefix of none of the variables", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", "id"); + const dt = new DeploymentTemplate("{ 'variables': { 'apples': 'APPLES', 'bananas': 88 } }", fakeId); assert.deepStrictEqual([], dt.topLevelScope.findVariableDefinitionsWithPrefix("ca")); }); }); suite("getContextFromDocumentLineAndColumnIndexes(number, number)", () => { test("with empty deployment template", () => { - const dt = new DeploymentTemplate("", "id"); - const context = dt.getContextFromDocumentLineAndColumnIndexes(0, 0); + const dt = new DeploymentTemplate("", fakeId); + const context = dt.getContextFromDocumentLineAndColumnIndexes(0, 0, undefined); assert(context); assert.equal(0, context.documentLineIndex); assert.equal(0, context.documentColumnIndex); @@ -1050,35 +1053,35 @@ suite("DeploymentTemplate", () => { suite("findReferences(Reference.Type, string)", () => { test("with parameter type and no matching parameter definition", () => { - const dt = new DeploymentTemplate(`{ "parameters": { "pName": {} } }`, "id"); + const dt = new DeploymentTemplate(`{ "parameters": { "pName": {} } }`, fakeId); const list: ReferenceList = findReferences(dt, DefinitionKind.Parameter, "dontMatchMe", dt.topLevelScope); assert(list); assert.deepStrictEqual(list.kind, DefinitionKind.Parameter); - assert.deepStrictEqual(list.spans, []); + assert.deepStrictEqual(list.references, []); }); test("with parameter type and matching parameter definition", () => { - const dt = new DeploymentTemplate(`{ "parameters": { "pName": {} } }`, "id"); + const dt = new DeploymentTemplate(`{ "parameters": { "pName": {} } }`, fakeId); const list: ReferenceList = findReferences(dt, DefinitionKind.Parameter, "pName", dt.topLevelScope); assert(list); assert.deepStrictEqual(list.kind, DefinitionKind.Parameter); - assert.deepStrictEqual(list.spans, [new Language.Span(19, 5)]); + assert.deepStrictEqual(list.references.map(r => r.span), [new Language.Span(19, 5)]); }); test("with variable type and no matching variable definition", () => { - const dt = new DeploymentTemplate(`{ "variables": { "vName": {} } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "vName": {} } }`, fakeId); const list: ReferenceList = findReferences(dt, DefinitionKind.Variable, "dontMatchMe", dt.topLevelScope); assert(list); assert.deepStrictEqual(list.kind, DefinitionKind.Variable); - assert.deepStrictEqual(list.spans, []); + assert.deepStrictEqual(list.references.map(r => r.span), []); }); test("with variable type and matching variable definition", () => { - const dt = new DeploymentTemplate(`{ "variables": { "vName": {} } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "vName": {} } }`, fakeId); const list: ReferenceList = findReferences(dt, DefinitionKind.Variable, "vName", dt.topLevelScope); assert(list); assert.deepStrictEqual(list.kind, DefinitionKind.Variable); - assert.deepStrictEqual(list.spans, [new Language.Span(18, 5)]); + assert.deepStrictEqual(list.references.map(r => r.span), [new Language.Span(18, 5)]); }); }); // findReferences @@ -1096,7 +1099,7 @@ suite("DeploymentTemplate", () => { }); test("with deploymentTemplate", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); assert.deepStrictEqual(visitor.referenceSpans, []); }); @@ -1136,21 +1139,21 @@ suite("DeploymentTemplate", () => { suite("visitStringValue(Json.StringValue)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); // tslint:disable-next-line:no-any assert.throws(() => { visitor.visitStringValue(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); // tslint:disable-next-line:no-any assert.throws(() => { visitor.visitStringValue(undefined); }); }); test("with non-TLE string", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); const variables: Json.StringValue = Json.asObjectValue(dt.jsonParseResult.value)!.properties[0].nameValue; visitor.visitStringValue(variables); @@ -1158,7 +1161,7 @@ suite("DeploymentTemplate", () => { }); test("with TLE string with reference() call", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[reference('test')]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); const dtObject: Json.ObjectValue | undefined = Json.asObjectValue(dt.jsonParseResult.value); const variablesObject: Json.ObjectValue | undefined = Json.asObjectValue(dtObject!.getPropertyValue("variables")); @@ -1169,7 +1172,7 @@ suite("DeploymentTemplate", () => { }); test("with TLE string with reference() call inside concat() call", () => { - const dt = new DeploymentTemplate(`{ "variables": { "a": "[concat(reference('test'))]" } }`, "id"); + const dt = new DeploymentTemplate(`{ "variables": { "a": "[concat(reference('test'))]" } }`, fakeId); const visitor = new ReferenceInVariableDefinitionsVisitor(dt); const dtObject: Json.ObjectValue | undefined = Json.asObjectValue(dt.jsonParseResult.value); const variablesObject: Json.ObjectValue | undefined = Json.asObjectValue(dtObject!.getPropertyValue("variables")); @@ -1205,12 +1208,12 @@ suite("DeploymentTemplate", () => { // console.log(`Testing index ${index}`); try { // Just make sure nothing throws - let dt = new DeploymentTemplate(json, "id"); - let pc = dt.getContextFromDocumentCharacterIndex(index); + let dt = new DeploymentTemplate(json, fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(index, undefined); pc.getReferences(); pc.getSignatureHelp(); pc.tleInfo; - pc.getReferenceSiteInfo(); + pc.getReferenceSiteInfo(true); pc.getHoverInfo(); pc.getCompletionItems(); } catch (err) { diff --git a/test/JSON.test.ts b/test/JSON.test.ts index a55683158..13550b5b1 100644 --- a/test/JSON.test.ts +++ b/test/JSON.test.ts @@ -6,6 +6,7 @@ import * as assert from "assert"; import { basic, Json, Language, Utilities } from "../extension.bundle"; +import { testStringAtEachIndex } from "./support/testStringAtEachIndex"; /** * Convert the provided text string into a sequence of basic Tokens. @@ -753,4 +754,286 @@ suite("JSON", () => { colonTest(0); colonTest(7); }); + + suite("getLastTokenOnLine", () => { + function createGetLastTokenOnLineTest( + testName: string, + json: string, + expectedTokenTypesIncludingComments: (Json.TokenType | undefined)[], + expectedTokenTypesIgnoringComments?: (Json.TokenType | undefined)[] + ): void { + if (!expectedTokenTypesIgnoringComments) { + expectedTokenTypesIgnoringComments = expectedTokenTypesIncludingComments; + } + + createTest(`${testName}, including comments`, Json.Comments.includeCommentTokens, expectedTokenTypesIncludingComments); + createTest(`${testName}, ignoring comments`, Json.Comments.ignoreCommentTokens, expectedTokenTypesIgnoringComments); + + function createTest(name: string, comments: Json.Comments, expected: (Json.TokenType | undefined)[]): void { + test(name, () => { + const result = Json.parse(json); + const lines = result.lineLengths.length; + const lastTokens: (Json.Token | undefined)[] = []; + for (let i = 0; i < lines; ++i) { + lastTokens[i] = result.getLastTokenOnLine(i, comments); + } + assert.deepStrictEqual(lastTokens.map(t => t?.type), expected); + }); + } + } + + suite("simple and line comments", () => { + createGetLastTokenOnLineTest( + "simple", + `{ + hi: "there" + }`, + [ + Json.TokenType.LeftCurlyBracket, + Json.TokenType.QuotedString, + Json.TokenType.RightCurlyBracket + ] + ); + createGetLastTokenOnLineTest( + "line comments", + `{ // one + hi: "there" // two + }// three`, + [ + Json.TokenType.Comment, + Json.TokenType.Comment, + Json.TokenType.Comment + ], + [ + Json.TokenType.LeftCurlyBracket, + Json.TokenType.QuotedString, + Json.TokenType.RightCurlyBracket + ] + ); + createGetLastTokenOnLineTest( + "empty string", + ``, + [ + undefined, + ], + [ + undefined, + ] + ); + createGetLastTokenOnLineTest( + "just a line comment", + `// hi`, + [ + Json.TokenType.Comment, + ], + [ + undefined, + ] + ); + createGetLastTokenOnLineTest( + "single line", + `hi`, + [ + Json.TokenType.Literal, + ] + ); + createGetLastTokenOnLineTest( + "single line plus line comment", + `hi //there`, + [ + Json.TokenType.Comment, + ], + [ + Json.TokenType.Literal, + ] + ); + createGetLastTokenOnLineTest( + "blank line at end", + `{ // one + }// three + `, + [ + Json.TokenType.Comment, + Json.TokenType.Comment, + undefined, + ], + [ + Json.TokenType.LeftCurlyBracket, + Json.TokenType.RightCurlyBracket, + undefined, + ] + ); + createGetLastTokenOnLineTest( + "blank lines", + ` + { // one + + hi: "there" // two + + }// three + `, + [ + undefined, + Json.TokenType.Comment, + undefined, + Json.TokenType.Comment, + undefined, + Json.TokenType.Comment, + undefined, + ], + [ + undefined, + Json.TokenType.LeftCurlyBracket, + undefined, + Json.TokenType.QuotedString, + undefined, + Json.TokenType.RightCurlyBracket, + undefined, + ] + ); + }); + suite("block comments", () => { + createGetLastTokenOnLineTest( + "just single line block comment", + `/* { hi: "there" } */`, + [ + Json.TokenType.Comment, + ], + [ + undefined + ] + ); + createGetLastTokenOnLineTest( + "just multiline block comment", + `/* { + hi: "there" + } */`, + [ + Json.TokenType.Comment, + Json.TokenType.Comment, + Json.TokenType.Comment + ], + [ + undefined, + undefined, + undefined + ] + ); + createGetLastTokenOnLineTest( + "block comments", + `{ /* one */ + hi: "there" /* two + still a comment + last of coment */ hi: "again" + } /* three */ `, + [ + Json.TokenType.Comment, + Json.TokenType.Comment, + Json.TokenType.Comment, + Json.TokenType.QuotedString, + Json.TokenType.Comment, + ], + [ + Json.TokenType.LeftCurlyBracket, + Json.TokenType.QuotedString, + undefined, + Json.TokenType.QuotedString, + Json.TokenType.RightCurlyBracket + ] + ); + createGetLastTokenOnLineTest( + "single line plus block comment", + `hi/*there*/`, + [ + Json.TokenType.Comment, + ], + [ + Json.TokenType.Literal, + ] + ); + createGetLastTokenOnLineTest( + "blank line at end", + `{ /* one */ + }/* two */ + `, + [ + Json.TokenType.Comment, + Json.TokenType.Comment, + undefined, + ], + [ + Json.TokenType.LeftCurlyBracket, + Json.TokenType.RightCurlyBracket, + undefined, + ] + ); + }); + }); + + // getCommentTokenAtDocumentIndex ======================= + + suite("getCommentTokenAtDocumentIndex", () => { + function createGetCommentTokenAtDocumentIndexTest( + testName: string, + containsBehavior: Language.Contains, + jsonWithMarkers: string + ): void { + test(testName, () => + testStringAtEachIndex( + jsonWithMarkers, + { + true: true, + false: false + }, + (text, index) => { + const parseResults = Json.parse(text); + const token = parseResults.getCommentTokenAtDocumentIndex(index, containsBehavior); + return !!token; + }) + ); + } + + suite("enclosed", () => { + createGetCommentTokenAtDocumentIndexTest( + "simple", + Language.Contains.enclosed, + `{ + hi: "there" + }` + ); + createGetCommentTokenAtDocumentIndexTest( + "line comments", + Language.Contains.enclosed, + `{ // one + hi: "there" // two + }// three`); + createGetCommentTokenAtDocumentIndexTest( + "block comments", + Language.Contains.enclosed, + `{ /* one */ + hi: "there" /* two + still a comment + last of coment */ hi: "again" + }` + ); + }); + + suite("strict", () => { + createGetCommentTokenAtDocumentIndexTest( + "line comments", + Language.Contains.strict, + `{ // one + hi: "there" // two + }// three`); + createGetCommentTokenAtDocumentIndexTest( + "block comments", + Language.Contains.strict, + `{ /* one */ + hi: "there" /* two + still a comment + last of coment */ hi: "again" + }` + ); + }); + }); }); diff --git a/test/Language.test.ts b/test/Language.test.ts index 479376cb0..c587b7886 100644 --- a/test/Language.test.ts +++ b/test/Language.test.ts @@ -8,6 +8,7 @@ import * as assert from "assert"; import { Language } from "../extension.bundle"; +const Contains = Language.Contains; const IssueKind = Language.IssueKind; suite("Language", () => { @@ -30,24 +31,76 @@ suite("Language", () => { }); suite("contains()", () => { - test("With index less than startIndex", () => { - assert.deepStrictEqual(false, new Language.Span(3, 4).contains(2)); - }); + suite("Contains.strict", () => { + test("With index less than startIndex", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(2, Contains.strict)); + }); - test("With index equal to startIndex", () => { - assert(new Language.Span(3, 4).contains(3)); - }); + test("With index equal to startIndex", () => { + assert(new Language.Span(3, 4).contains(3, Contains.strict)); + }); + + test("With index between the start and end indexes", () => { + assert(new Language.Span(3, 4).contains(5, Contains.strict)); + }); + + test("With index equal to endIndex", () => { + assert(new Language.Span(3, 4).contains(6, Contains.strict)); + }); - test("With index between the start and end indexes", () => { - assert(new Language.Span(3, 4).contains(5)); + test("With index directly after end index", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(7, Contains.strict)); + }); }); - test("With index equal to endIndex", () => { - assert(new Language.Span(3, 4).contains(6)); + suite("Contains.extended", () => { + test("With index less than startIndex", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(2, Contains.extended)); + }); + + test("With index equal to startIndex", () => { + assert(new Language.Span(3, 4).contains(3, Contains.extended)); + }); + + test("With index between the start and end indexes", () => { + assert(new Language.Span(3, 4).contains(5, Contains.extended)); + }); + + test("With index equal to endIndex", () => { + assert(new Language.Span(3, 4).contains(6, Contains.extended)); + }); + + test("With index directly after end index", () => { + // Extended, so this should be true + assert.deepStrictEqual(true, new Language.Span(3, 4).contains(7, Contains.extended)); + }); + + test("With index two after end index", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(8, Contains.extended)); + }); }); - test("With index directly after end index", () => { - assert.deepStrictEqual(false, new Language.Span(3, 4).contains(7)); + suite("Contains.enclosed", () => { + test("With index less than startIndex", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(2, Contains.enclosed)); + }); + + test("With index equal to startIndex", () => { + // With enclosed, this should be false + assert.equal(new Language.Span(3, 4).contains(3, Contains.enclosed), false); + }); + + test("With index between the start and end indexes", () => { + assert(new Language.Span(3, 4).contains(5, Contains.enclosed)); + }); + + test("With index equal to endIndex", () => { + assert(new Language.Span(3, 4).contains(6, Contains.enclosed)); + }); + + test("With index directly after end index", () => { + assert.deepStrictEqual(false, new Language.Span(3, 4).contains(7, Contains.enclosed)); + }); }); }); @@ -73,6 +126,104 @@ suite("Language", () => { }); }); + suite("intersect()", () => { + + test("With null", () => { + let s = Language.Span.fromStartAndAfterEnd(5, 7); + assert.deepStrictEqual(s.intersect(undefined), undefined); + }); + + test("With same span", () => { + let s = Language.Span.fromStartAndAfterEnd(5, 7); + assert.deepEqual(s, s.intersect(s)); + }); + + test("With equal span", () => { + let s = Language.Span.fromStartAndAfterEnd(5, 7); + assert.deepEqual(s, s.intersect(new Language.Span(5, 7))); + }); + + test("second span to left", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(Language.Span.fromStartAndAfterEnd(0, 9)), + undefined + ); + }); + + test("second touches the left", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(Language.Span.fromStartAndAfterEnd(0, 10)), + // Two results could be argued here: len 0 span at 10, or undefined + // We'll go with the former until sometimes finds a reason why it should + // be different + new Language.Span(10, 0) + ); + }); + + test("second span to left and overlap", () => { + assert.deepEqual( + new Language.Span(10, 20).intersect(new Language.Span(0, 11)), + new Language.Span(10, 1) + ); + }); + + test("second span is superset", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(Language.Span.fromStartAndAfterEnd(0, 21)), + Language.Span.fromStartAndAfterEnd(10, 20) + ); + }); + + test("second span is subset", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(new Language.Span(11, 8)), + new Language.Span(11, 8) + ); + }); + + test("second span is len 0 subset, touching on the left", () => { + assert.deepEqual( + new Language.Span(10, 10).intersect(new Language.Span(10, 0)), + new Language.Span(10, 0) + ); + }); + + test("second span is len 0 subset, touching on the right", () => { + assert.deepEqual( + new Language.Span(10, 10).intersect(new Language.Span(20, 0)), + new Language.Span(20, 0) + ); + }); + + test("second span to right and overlapping", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(new Language.Span(19, 10)), + new Language.Span(19, 1) + ); + }); + + test("second span to right", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(new Language.Span(21, 9)), + undefined + ); + }); + + test("length 0", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(new Language.Span(15, 0)), + new Language.Span(15, 0) + ); + }); + + test("length 1", () => { + assert.deepEqual( + Language.Span.fromStartAndAfterEnd(10, 20).intersect(new Language.Span(15, 1)), + new Language.Span(15, 1) + ); + }); + }); + suite("translate()", () => { test("with 0 movement", () => { const span = new Language.Span(1, 2); diff --git a/test/editParameterFile.test.ts b/test/ParameterFileGeneration.test.ts similarity index 82% rename from test/editParameterFile.test.ts rename to test/ParameterFileGeneration.test.ts index 4a92aa6a4..d2f93fbae 100644 --- a/test/editParameterFile.test.ts +++ b/test/ParameterFileGeneration.test.ts @@ -5,12 +5,12 @@ // tslint:disable:max-func-body-length import * as assert from "assert"; -import { createParameterFileContents, createParameterProperty } from "../extension.bundle"; +import { createParameterFileContents, createParameterFromTemplateParameter } from "../extension.bundle"; import { IDeploymentParameterDefinition, IDeploymentTemplate } from "./support/diagnostics"; import { normalizeString } from "./support/normalizeString"; import { parseTemplate } from "./support/parseTemplate"; -suite("editParameterFile tests", () => { +suite("parameterFileGeneration tests", () => { suite("createParameterFileContents", () => { function testCreateFile( testName: string, @@ -38,6 +38,15 @@ suite("editParameterFile tests", () => { testName: string, parameterDefinition: Partial, expectedContents: string + ): void { + testSinglePropertyWithIndent(testName, 4, parameterDefinition, expectedContents); + } + + function testSinglePropertyWithIndent( + testName: string, + spacesPerIndent: number, + parameterDefinition: Partial, + expectedContents: string ): void { test(testName, async () => { expectedContents = normalizeString(expectedContents); @@ -58,7 +67,7 @@ suite("editParameterFile tests", () => { const foundDefinition = dt.topLevelScope.getParameterDefinition(parameterName); assert(foundDefinition); // tslint:disable-next-line:no-non-null-assertion - const param = createParameterProperty(dt, foundDefinition!, 4); + const param = createParameterFromTemplateParameter(dt, foundDefinition!, spacesPerIndent); assert.equal(param, expectedContents); }); } @@ -223,5 +232,40 @@ suite("editParameterFile tests", () => { }); }); + + suite("with indentation", () => { + testSinglePropertyWithIndent( + "indent = 0", + 0, + { + type: "object", + defaultValue: "abc" + }, + `"parameter1": { +"value": "abc" +}`); + + testSinglePropertyWithIndent( + "indent = 4", + 4, + { + type: "object", + defaultValue: "abc" + }, + `"parameter1": { + "value": "abc" +}`); + + testSinglePropertyWithIndent( + "indent = 8", + 8, + { + type: "object", + defaultValue: "abc" + }, + `"parameter1": { + "value": "abc" +}`); + }); }); }); diff --git a/test/ParametersPositionContext.test.ts b/test/ParametersPositionContext.test.ts new file mode 100644 index 000000000..2145d5369 --- /dev/null +++ b/test/ParametersPositionContext.test.ts @@ -0,0 +1,171 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable: max-func-body-length s + +import { Uri } from 'vscode'; +import { DeploymentParameters } from '../extension.bundle'; +import { testStringAtEachIndex } from './support/testStringAtEachIndex'; + +suite("ParametersPositionContext", () => { + suite("canAddPropertyHere", () => { + /* parameterFileContents = Parameter file with and markers + indicating where we expect a true or false return from canAddPropertyHere, e.g.: + + { + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "p1": { + "value": { + "abc": "def" + } + } + } + } + */ + function createCanAddPropertyHereTest( + testName: string, + parameterFileWithMarkers: string, + ): void { + test(testName, async () => { + await testStringAtEachIndex( + parameterFileWithMarkers, + { + true: true, + false: false + }, + (text: string, index: number) => { + const dp = new DeploymentParameters(text, Uri.file("test parameter file")); + const pc = dp.getContextFromDocumentCharacterIndex(index, undefined); + const canAddHere = pc.canAddPropertyHere; + return canAddHere; + } + ); + }); + } + + createCanAddPropertyHereTest( + "No parameters section", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0" + }`); + + createCanAddPropertyHereTest( + "Empty parameters section with whitespace", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} + }`); + + createCanAddPropertyHereTest( + "Empty parameters section, no whitespace or newlines anywhere", + `{$schema:"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",` + + `"contentVersion":"1.0.0.0",` + + `"parameters":{}` + + `}`); + + createCanAddPropertyHereTest( + "Empty parameters section with newline", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + } + }`); + + createCanAddPropertyHereTest( + "Empty parameters section with blank line", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + + } + }`); + + createCanAddPropertyHereTest( + "Empty parameters section with block comment, no whitespace or newlines anywhere", + `{$schema:"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",` + + `"contentVersion":"1.0.0.0",` + + `"parameters":{/* comment */}` + + `}`); + + createCanAddPropertyHereTest( + "Empty parameters section with two block comments, no whitespace or newlines anywhere", + `{$schema:"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",` + + `"contentVersion":"1.0.0.0",` + + `"parameters":{/* one *//* two */}` + + `}`); + + createCanAddPropertyHereTest( + "Empty parameters section with line comment", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // This is a comment + } + }`); + + createCanAddPropertyHereTest( + "Empty parameters section with two line comments", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // This is a comment + // This is a comment + } + }`); + + createCanAddPropertyHereTest( + "Empty parameters section with block and line comments", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // This is a comment + + /* And this is another comment: + Hi, Mom */ + + } + }`); + + createCanAddPropertyHereTest( + "Single parameter", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "p1": { + "value": { + "abc": "def" + } + } + } + }`); + + createCanAddPropertyHereTest( + "Multiple parameters", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "p1": { + "value": { + "abc": "def" + } + }, + + "p1": { + "value": "ghi" + } + } + }`); + }); +}); diff --git a/test/Reference.test.ts b/test/Reference.test.ts index b30e9df4d..c62cbaaa3 100644 --- a/test/Reference.test.ts +++ b/test/Reference.test.ts @@ -5,10 +5,13 @@ // tslint:disable:no-unused-expression no-null-keyword max-func-body-length import * as assert from "assert"; -import { DefinitionKind, Language, ReferenceList } from "../extension.bundle"; +import { Uri } from "vscode"; +import { DefinitionKind, DeploymentTemplate, Language, ReferenceList } from "../extension.bundle"; suite("Reference", () => { suite("List", () => { + const document = new DeploymentTemplate("", Uri.parse("fake")); + suite("constructor(Reference.Type, Span[])", () => { test("with null type", () => { // tslint:disable-next-line:no-any @@ -28,39 +31,27 @@ suite("Reference", () => { test("with undefined spans", () => { const list = new ReferenceList(DefinitionKind.Parameter, undefined); assert.deepStrictEqual(list.kind, DefinitionKind.Parameter); - assert.deepStrictEqual(list.spans, []); + assert.deepStrictEqual(list.references, []); assert.deepStrictEqual(list.length, 0); }); test("with empty spans", () => { const list = new ReferenceList(DefinitionKind.Parameter, []); assert.deepStrictEqual(list.kind, DefinitionKind.Parameter); - assert.deepStrictEqual(list.spans, []); + assert.deepStrictEqual(list.references, []); assert.deepStrictEqual(list.length, 0); }); test("with non-empty spans", () => { - const list = new ReferenceList(DefinitionKind.Parameter, [new Language.Span(0, 1), new Language.Span(2, 3)]); + const list = new ReferenceList(DefinitionKind.Parameter, [ + { document, span: new Language.Span(0, 1) }, + { document, span: new Language.Span(2, 3) }]); assert.deepStrictEqual(list.kind, DefinitionKind.Parameter); - assert.deepStrictEqual(list.spans, [new Language.Span(0, 1), new Language.Span(2, 3)]); + assert.deepStrictEqual(list.references.map(r => r.span), [new Language.Span(0, 1), new Language.Span(2, 3)]); assert.deepStrictEqual(list.length, 2); }); }); - suite("add(Span)", () => { - test("with null", () => { - const list = new ReferenceList(DefinitionKind.Variable); - // tslint:disable-next-line:no-any - assert.throws(() => { list.add(null); }); - }); - - test("with undefined", () => { - const list = new ReferenceList(DefinitionKind.Variable); - // tslint:disable-next-line:no-any - assert.throws(() => { list.add(undefined); }); - }); - }); - suite("addAll(Reference.List)", () => { test("with null", () => { const list = new ReferenceList(DefinitionKind.Variable); @@ -78,7 +69,7 @@ suite("Reference", () => { const list = new ReferenceList(DefinitionKind.Variable); list.addAll(new ReferenceList(DefinitionKind.Variable)); assert.deepStrictEqual(list.length, 0); - assert.deepStrictEqual(list.spans, []); + assert.deepStrictEqual(list.references, []); }); test("with empty list of a different type", () => { @@ -88,8 +79,8 @@ suite("Reference", () => { test("with non-empty list", () => { const list = new ReferenceList(DefinitionKind.Variable); - list.addAll(new ReferenceList(DefinitionKind.Variable, [new Language.Span(10, 20)])); - assert.deepStrictEqual(list.spans, [new Language.Span(10, 20)]); + list.addAll(new ReferenceList(DefinitionKind.Variable, [{ document, span: new Language.Span(10, 20) }])); + assert.deepStrictEqual(list.references.map(r => r.span), [new Language.Span(10, 20)]); }); }); @@ -101,19 +92,19 @@ suite("Reference", () => { }); test("with non-empty list", () => { - const list = new ReferenceList(DefinitionKind.Parameter, [new Language.Span(10, 20)]); + const list = new ReferenceList(DefinitionKind.Parameter, [{ document, span: new Language.Span(10, 20) }]); const list2 = list.translate(17); - assert.deepStrictEqual(list2, new ReferenceList(DefinitionKind.Parameter, [new Language.Span(27, 20)])); + assert.deepStrictEqual(list2, new ReferenceList(DefinitionKind.Parameter, [{ document, span: new Language.Span(27, 20) }])); }); test("with null movement", () => { - const list = new ReferenceList(DefinitionKind.Parameter, [new Language.Span(10, 20)]); + const list = new ReferenceList(DefinitionKind.Parameter, [{ document, span: new Language.Span(10, 20) }]); // tslint:disable-next-line:no-any assert.throws(() => { list.translate(null); }); }); test("with undefined movement", () => { - const list = new ReferenceList(DefinitionKind.Parameter, [new Language.Span(10, 20)]); + const list = new ReferenceList(DefinitionKind.Parameter, [{ document, span: new Language.Span(10, 20) }]); // tslint:disable-next-line:no-any assert.throws(() => { list.translate(undefined); }); }); diff --git a/test/TLE.test.ts b/test/TLE.test.ts index 61df80c4d..d3b517121 100644 --- a/test/TLE.test.ts +++ b/test/TLE.test.ts @@ -6,13 +6,16 @@ // tslint:disable:no-non-null-assertion import * as assert from "assert"; -import { AzureRMAssets, BuiltinFunctionMetadata, DefinitionKind, DeploymentTemplate, FindReferencesVisitor, FunctionsMetadata, IncorrectArgumentsCountIssue, IncorrectFunctionArgumentCountVisitor, Language, nonNullValue, PositionContext, ReferenceList, ScopeContext, TemplateScope, TLE, UndefinedParameterAndVariableVisitor, UndefinedVariablePropertyVisitor, UnrecognizedBuiltinFunctionIssue, UnrecognizedFunctionVisitor } from "../extension.bundle"; +import { Uri } from "vscode"; +import { AzureRMAssets, BuiltinFunctionMetadata, DefinitionKind, DeploymentTemplate, FindReferencesVisitor, FunctionsMetadata, IncorrectArgumentsCountIssue, IncorrectFunctionArgumentCountVisitor, Language, nonNullValue, ReferenceList, ScopeContext, TemplatePositionContext, TemplateScope, TLE, UndefinedParameterAndVariableVisitor, UndefinedVariablePropertyVisitor, UnrecognizedBuiltinFunctionIssue, UnrecognizedFunctionVisitor } from "../extension.bundle"; import { IDeploymentTemplate } from "./support/diagnostics"; import { parseTemplate } from "./support/parseTemplate"; const IssueKind = Language.IssueKind; const tleSyntax = IssueKind.tleSyntax; +const fakeId = Uri.file("https://fake-id"); + suite("TLE", () => { const emptyScope = new TemplateScope(ScopeContext.TopLevel, [], [], [], "empty scope"); @@ -21,6 +24,29 @@ suite("TLE", () => { return TLE.Parser.parse(stringValue, scope); } + suite("isExpression", () => { + function createIsExpressionTest(unquotedValue: string, expectedResult: boolean): void { + test(`"${unquotedValue}"`, () => { + const result = TLE.isTleExpression(unquotedValue); + assert.equal(result, expectedResult); + }); + } + + createIsExpressionTest("", false); + createIsExpressionTest("[", false); + createIsExpressionTest("['hi'", false); + createIsExpressionTest("]", false); + createIsExpressionTest("[[", false); + createIsExpressionTest("'hi'", false); + createIsExpressionTest(" []", false); + createIsExpressionTest("[] ", false); + createIsExpressionTest("[[] ", false); + createIsExpressionTest("[[]", false); + + createIsExpressionTest("[]", true); + createIsExpressionTest("['hi']", true); + }); + suite("StringValue", () => { suite("constructor(tle.Token)", () => { test("with undefined token", () => { @@ -334,44 +360,44 @@ suite("TLE", () => { suite("BraceHighlighter", () => { function getHighlights(template: DeploymentTemplate, documentCharacterIndex: number): number[] { - const context = template.getContextFromDocumentCharacterIndex(documentCharacterIndex); + const context = template.getContextFromDocumentCharacterIndex(documentCharacterIndex, undefined); return TLE.BraceHighlighter.getHighlightCharacterIndexes(context); } suite("getHighlightCharacterIndexes(number,TLEParseResult)", () => { test("with quoted string that isn't a TLE", () => { - let template = new DeploymentTemplate("\"Hello world\"", "id"); + let template = new DeploymentTemplate("\"Hello world\"", fakeId); assert.deepStrictEqual([], getHighlights(template, 0)); assert.deepStrictEqual([], getHighlights(template, 5)); assert.deepStrictEqual([], getHighlights(template, 11)); }); test("with left square bracket", () => { - let template = new DeploymentTemplate("\"[", "id"); + let template = new DeploymentTemplate("\"[", fakeId); assert.deepStrictEqual([], getHighlights(template, 0)); assert.deepStrictEqual([1], getHighlights(template, 1)); assert.deepStrictEqual([], getHighlights(template, 2)); }); test("with empty TLE", () => { - let template = new DeploymentTemplate("\"[]\"", "id"); + let template = new DeploymentTemplate("\"[]\"", fakeId); assert.deepStrictEqual([1, 2], getHighlights(template, 1), "When the caret is before a TLE's left square bracket, then the left and right square brackets should be highlighted."); assert.deepStrictEqual([], getHighlights(template, 2), "When the caret is to the right of a TLE's left square bracket and to the left of the right square bracket, nothing should be highlighted."); assert.deepStrictEqual([1, 2], getHighlights(template, 3), "When the caret is after a TLE's right square bracket, then the left and right square brackets should be highlighted."); }); test("with function with no parenthesis", () => { - let template = new DeploymentTemplate("\"[concat", "id"); + let template = new DeploymentTemplate("\"[concat", fakeId); assert.deepStrictEqual([], getHighlights(template, 8)); }); test("with function with left parenthesis but no right parenthesis", () => { - let template = new DeploymentTemplate("\"[concat(", "id"); + let template = new DeploymentTemplate("\"[concat(", fakeId); assert.deepStrictEqual([8], getHighlights(template, 8)); }); test("with function with left and right parenthesis", () => { - let template = new DeploymentTemplate("\"[concat()", "id"); + let template = new DeploymentTemplate("\"[concat()", fakeId); assert.deepStrictEqual([8, 9], getHighlights(template, "\"[concat".length), "Both left and right parentheses should be highlighted when the caret is before the left parenthesis."); assert.deepStrictEqual([], getHighlights(template, 9)); assert.deepStrictEqual([8, 9], getHighlights(template, "\"[concat()".length), "Both left and right parentheses should be highlighted when the caret is after the right parenthesis."); @@ -392,7 +418,7 @@ suite("TLE", () => { }); test("with deployment template", () => { - const dt = new DeploymentTemplate("\"{}\"", "id"); + const dt = new DeploymentTemplate("\"{}\"", fakeId); const visitor = new UndefinedParameterAndVariableVisitor(dt.topLevelScope); assert.deepStrictEqual(visitor.errors, []); }); @@ -400,21 +426,21 @@ suite("TLE", () => { suite("visitString(StringValue)", () => { test("with undefined", () => { - const dt = new DeploymentTemplate("\"{}\"", "id"); + const dt = new DeploymentTemplate("\"{}\"", fakeId); const visitor = new UndefinedParameterAndVariableVisitor(dt.topLevelScope); // tslint:disable-next-line:no-any assert.throws(() => { visitor.visitString(undefined); }); }); test("with undefined", () => { - const dt = new DeploymentTemplate("\"{}\"", "id"); + const dt = new DeploymentTemplate("\"{}\"", fakeId); const visitor = new UndefinedParameterAndVariableVisitor(dt.topLevelScope); // tslint:disable-next-line:no-any assert.throws(() => { visitor.visitString(undefined); }); }); test("with empty StringValue in parameters() function", () => { - const dt = new DeploymentTemplate("\"{}\"", "id"); + const dt = new DeploymentTemplate("\"{}\"", fakeId); const visitor = new UndefinedParameterAndVariableVisitor(dt.topLevelScope); const stringValue = new TLE.StringValue(TLE.Token.createQuotedString(17, "''")); @@ -438,7 +464,7 @@ suite("TLE", () => { }); test("with empty StringValue in variables() function", () => { - const dt = new DeploymentTemplate("\"{}\"", "id"); + const dt = new DeploymentTemplate("\"{}\"", fakeId); const visitor = new UndefinedParameterAndVariableVisitor(dt.topLevelScope); const stringValue = new TLE.StringValue(TLE.Token.createQuotedString(17, "''")); @@ -2145,22 +2171,22 @@ suite("TLE", () => { suite("UndefinedVariablePropertyVisitor", () => { suite("visitPropertyAccess(TLE.PropertyAccess)", () => { test("with child property access from undefined variable reference", () => { - const dt = new DeploymentTemplate(`{ "a": "[variables('v1').apples]" }`, "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[variables('v1').app`.length); + const dt = new DeploymentTemplate(`{ "a": "[variables('v1').apples]" }`, fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[variables('v1').app`.length, undefined); const visitor = UndefinedVariablePropertyVisitor.visit(context.tleInfo!.tleValue, dt.topLevelScope); assert.deepStrictEqual(visitor.errors, [], "No errors should be reported for a property access to an undefined variable, because the top priority error for the developer to address is the undefined variable reference."); }); test("with grandchild property access from undefined variable reference", () => { - const dt = new DeploymentTemplate(`{ "a": "[variables('v1').apples.bananas]" }`, "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[variables('v1').apples.ban`.length); + const dt = new DeploymentTemplate(`{ "a": "[variables('v1').apples.bananas]" }`, fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[variables('v1').apples.ban`.length, undefined); const visitor = UndefinedVariablePropertyVisitor.visit(context.tleInfo!.tleValue, dt.topLevelScope); assert.deepStrictEqual(visitor.errors, [], "No errors should be reported for a property access to an undefined variable, because the top priority error for the developer to address is the undefined variable reference."); }); test("with child property access from variable reference to non-object variable", () => { - const dt = new DeploymentTemplate(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples]" }`, "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').app`.length); + const dt = new DeploymentTemplate(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples]" }`, fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').app`.length, undefined); const visitor = UndefinedVariablePropertyVisitor.visit(context.tleInfo!.tleValue, dt.topLevelScope); assert.deepStrictEqual( visitor.errors, @@ -2168,8 +2194,8 @@ suite("TLE", () => { }); test("with grandchild property access from variable reference to non-object variable", () => { - const dt = new DeploymentTemplate(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples.bananas]" }`, "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples.ban`.length); + const dt = new DeploymentTemplate(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples.bananas]" }`, fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "variables": { "v1": "blah" }, "a": "[variables('v1').apples.ban`.length, undefined); const visitor = UndefinedVariablePropertyVisitor.visit(context.tleInfo!.tleValue, dt.topLevelScope); assert.deepStrictEqual( visitor.errors, @@ -2199,7 +2225,7 @@ suite("TLE", () => { const dt = await parseTemplate(template); const param = dt.topLevelScope.getParameterDefinition("pName")!; assert(param); - const visitor = FindReferencesVisitor.visit(undefined, param, metadata); + const visitor = FindReferencesVisitor.visit(dt, undefined, param, metadata); assert(visitor); assert.deepStrictEqual(visitor.references, new ReferenceList(DefinitionKind.Parameter)); }); @@ -2209,7 +2235,7 @@ suite("TLE", () => { const param = dt.topLevelScope.getParameterDefinition("pName")!; assert(param); // tslint:disable-next-line:no-any - const visitor = FindReferencesVisitor.visit(undefined, param, metadata); + const visitor = FindReferencesVisitor.visit(dt, undefined, param, metadata); assert(visitor); assert.deepStrictEqual(visitor.references, new ReferenceList(DefinitionKind.Parameter)); }); @@ -2219,11 +2245,11 @@ suite("TLE", () => { const param = dt.topLevelScope.getParameterDefinition("pName")!; assert(param); const pr: TLE.ParseResult = parseExpressionWithScope(`"[parameters('pName')]"`, dt.topLevelScope); - const visitor = FindReferencesVisitor.visit(pr.expression, param, metadata); + const visitor = FindReferencesVisitor.visit(dt, pr.expression, param, metadata); assert(visitor); assert.deepStrictEqual( visitor.references, - new ReferenceList(DefinitionKind.Parameter, [new Language.Span(14, 5)])); + new ReferenceList(DefinitionKind.Parameter, [{ document: dt, span: new Language.Span(14, 5) }])); }); }); }); diff --git a/test/PositionContext.test.ts b/test/TemplatePositionContext.test.ts similarity index 84% rename from test/PositionContext.test.ts rename to test/TemplatePositionContext.test.ts index ab0e4b848..c0e3adc02 100644 --- a/test/PositionContext.test.ts +++ b/test/TemplatePositionContext.test.ts @@ -7,7 +7,8 @@ import * as assert from "assert"; import * as os from 'os'; -import { Completion, DeploymentTemplate, FunctionSignatureHelp, HoverInfo, IParameterDefinition, IReferenceSite, isVariableDefinition, IVariableDefinition, Json, Language, nonNullValue, PositionContext, TLE, UserFunctionMetadata, Utilities } from "../extension.bundle"; +import { Uri } from "vscode"; +import { Completion, DeploymentTemplate, FunctionSignatureHelp, HoverInfo, IParameterDefinition, IReferenceSite, isVariableDefinition, IVariableDefinition, Json, Language, nonNullValue, TemplatePositionContext, TLE, UserFunctionMetadata, Utilities } from "../extension.bundle"; import * as jsonTest from "./JSON.test"; import { IDeploymentTemplate } from "./support/diagnostics"; import { parseTemplate, parseTemplateWithMarkers } from "./support/parseTemplate"; @@ -16,68 +17,70 @@ import { allTestDataCompletionNames, allTestDataExpectedCompletions, expectedCon const IssueKind = Language.IssueKind; -suite("PositionContext", () => { +const fakeId = Uri.file("https://doc-id"); + +suite("TemplatePositionContext", () => { suite("fromDocumentLineAndColumnIndexes(DeploymentTemplate,number,number)", () => { test("with undefined deploymentTemplate", () => { // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(undefined, 1, 2); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(undefined, 1, 2, undefined); }); }); test("with undefined deploymentTemplate", () => { // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(undefined, 1, 2); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(undefined, 1, 2, undefined); }); }); test("with undefined documentLineIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, undefined, 2); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, undefined, 2, undefined); }); }); test("with undefined documentLineIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, undefined, 2); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, undefined, 2, undefined); }); }); test("with negative documentLineIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, -1, 2); }); + let dt = new DeploymentTemplate("{}", fakeId); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, -1, 2, undefined); }); }); test("with documentLineIndex equal to document line count", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); assert.deepStrictEqual(1, dt.lineCount); - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, 1, 0); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 1, 0, undefined); }); }); test("with undefined documentColumnIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, 0, undefined); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 0, undefined, undefined); }); }); test("with undefined documentColumnIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, 0, undefined); }); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 0, undefined, undefined); }); }); test("with negative documentColumnIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, 0, -2); }); + let dt = new DeploymentTemplate("{}", fakeId); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 0, -2, undefined); }); }); test("with documentColumnIndex greater than line length", () => { - let dt = new DeploymentTemplate("{}", "id"); - assert.throws(() => { PositionContext.fromDocumentLineAndColumnIndexes(dt, 0, 3); }); + let dt = new DeploymentTemplate("{}", fakeId); + assert.throws(() => { TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 0, 3, undefined); }); }); test("with valid arguments", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); let documentLineIndex = 0; let documentColumnIndex = 2; - let pc = PositionContext.fromDocumentLineAndColumnIndexes(dt, documentLineIndex, documentColumnIndex); + let pc = TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, documentLineIndex, documentColumnIndex, undefined); assert.deepStrictEqual(new Language.Position(0, 2), pc.documentPosition); assert.deepStrictEqual(0, pc.documentLineIndex); assert.deepStrictEqual(2, pc.documentColumnIndex); @@ -87,135 +90,135 @@ suite("PositionContext", () => { suite("fromDocumentCharacterIndex(DeploymentTemplate,number)", () => { test("with undefined deploymentTemplate", () => { // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(undefined, 1); }); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(undefined, 1, undefined); }); }); test("with undefined deploymentTemplate", () => { // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(undefined, 1); }); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(undefined, 1, undefined); }); }); test("with undefined documentCharacterIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(dt, undefined); }); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(dt, undefined, undefined); }); }); test("with undefined documentCharacterIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); // tslint:disable-next-line:no-any - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(dt, undefined); }); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(dt, undefined, undefined); }); }); test("with negative documentCharacterIndex", () => { - let dt = new DeploymentTemplate("{}", "id"); - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(dt, -1); }); + let dt = new DeploymentTemplate("{}", fakeId); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(dt, -1, undefined); }); }); test("with documentCharacterIndex greater than the maximum character index", () => { - let dt = new DeploymentTemplate("{}", "id"); - assert.throws(() => { PositionContext.fromDocumentCharacterIndex(dt, 3); }); + let dt = new DeploymentTemplate("{}", fakeId); + assert.throws(() => { TemplatePositionContext.fromDocumentCharacterIndex(dt, 3, undefined); }); }); test("with valid arguments", () => { - let dt = new DeploymentTemplate("{}", "id"); + let dt = new DeploymentTemplate("{}", fakeId); let documentCharacterIndex = 2; - let pc = PositionContext.fromDocumentCharacterIndex(dt, documentCharacterIndex); + let pc = TemplatePositionContext.fromDocumentCharacterIndex(dt, documentCharacterIndex, undefined); assert.deepStrictEqual(2, pc.documentCharacterIndex); }); }); suite("documentPosition", () => { test("with PositionContext from line and column indexes", () => { - const dt = new DeploymentTemplate("{\n}", "id"); - let pc = PositionContext.fromDocumentLineAndColumnIndexes(dt, 1, 0); + const dt = new DeploymentTemplate("{\n}", fakeId); + let pc = TemplatePositionContext.fromDocumentLineAndColumnIndexes(dt, 1, 0, undefined); assert.deepStrictEqual(new Language.Position(1, 0), pc.documentPosition); }); test("with PositionContext from characterIndex", () => { - let pc = PositionContext.fromDocumentCharacterIndex(new DeploymentTemplate("{\n}", "id"), 2); + let pc = TemplatePositionContext.fromDocumentCharacterIndex(new DeploymentTemplate("{\n}", fakeId), 2, undefined); assert.deepStrictEqual(new Language.Position(1, 0), pc.documentPosition); }); }); suite("documentCharacterIndex", () => { test("with PositionContext from line and column indexes", () => { - let pc = PositionContext.fromDocumentLineAndColumnIndexes(new DeploymentTemplate("{\n}", "id"), 1, 0); + let pc = TemplatePositionContext.fromDocumentLineAndColumnIndexes(new DeploymentTemplate("{\n}", fakeId), 1, 0, undefined); assert.deepStrictEqual(2, pc.documentCharacterIndex); }); test("with PositionContext from characterIndex", () => { - let pc = PositionContext.fromDocumentCharacterIndex(new DeploymentTemplate("{\n}", "id"), 2); + let pc = TemplatePositionContext.fromDocumentCharacterIndex(new DeploymentTemplate("{\n}", fakeId), 2, undefined); assert.deepStrictEqual(2, pc.documentCharacterIndex); }); }); suite("jsonToken", () => { test("with characterIndex in whitespace", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(1); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(1, undefined); assert.deepStrictEqual(undefined, pc.jsonToken); }); test("with characterIndex at the start of a LeftCurlyBracket", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(0); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(0, undefined); assert.deepStrictEqual(Json.LeftCurlyBracket(0), pc.jsonToken); }); test("with characterIndex at the start of a QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(2); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(2, undefined); assert.deepStrictEqual(pc.jsonToken, jsonTest.parseQuotedString(`'a'`, 2)); }); test("with characterIndex inside of a QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(3); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(3, undefined); assert.deepStrictEqual(pc.jsonToken, jsonTest.parseQuotedString(`'a'`, 2)); }); test("with characterIndex at the end of a closed QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a'", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(5); + let dt = new DeploymentTemplate("{ 'a'", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(5, undefined); assert.deepStrictEqual(pc.jsonToken, jsonTest.parseQuotedString(`'a'`, 2)); }); test("with characterIndex at the end of an unclosed QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(4); + let dt = new DeploymentTemplate("{ 'a", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(4, undefined); assert.deepStrictEqual(pc.jsonToken, jsonTest.parseQuotedString(`'a`, 2)); }); }); suite("tleParseResult", () => { test("with characterIndex in whitespace", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(1); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(1, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex at the start of a LeftCurlyBracket", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(0); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(0, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex at the start of a Colon", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(5); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(5, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex at the start of a non-TLE QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(2); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(2, undefined); assert.deepStrictEqual(TLE.Parser.parse("'a'", dt.topLevelScope), pc.tleInfo!.tleParseResult); }); test("with characterIndex at the start of a closed TLE QuotedString", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(17); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(17, undefined); const tleParseResult: TLE.ParseResult = pc.tleInfo!.tleParseResult; assert.deepStrictEqual(tleParseResult.errors, []); @@ -235,8 +238,8 @@ suite("PositionContext", () => { }); test("with characterIndex at the start of an unclosed TLE QuotedString", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(17); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(17, undefined); const tleParseResult: TLE.ParseResult = pc.tleInfo!.tleParseResult; assert.deepStrictEqual( @@ -262,78 +265,78 @@ suite("PositionContext", () => { suite("tleCharacterIndex", () => { test("with characterIndex at the start of a LeftCurlyBracket", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(0); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(0, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex in whitespace", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(1); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(1, undefined); assert.deepStrictEqual(undefined, pc.tleInfo); }); test("with characterIndex at the start of a non-TLE QuotedString", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(2); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(2, undefined); assert.deepStrictEqual(0, pc.tleInfo!.tleCharacterIndex); }); test("with characterIndex at the start of a TLE", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(17); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(17, undefined); assert.deepStrictEqual(0, pc.tleInfo!.tleCharacterIndex); }); test("with characterIndex inside of a TLE", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(21); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(21, undefined); assert.deepStrictEqual(pc.tleInfo!.tleCharacterIndex, 4); }); test("with characterIndex after the end of a closed TLE", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(32); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(32, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex after the end of an unclosed TLE", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(29); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(29, undefined); assert.deepStrictEqual(12, pc.tleInfo!.tleCharacterIndex); }); }); suite("tleValue", () => { test("with characterIndex at the start of a LeftCurlyBracket", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(0); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(0, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex in whitespace", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(1); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(1, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex at the start of a non-TLE QuotedString", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(2); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(2, undefined); assert.deepStrictEqual( pc.tleInfo!.tleValue, new TLE.StringValue(TLE.Token.createQuotedString(0, "'a'"))); }); test("with characterIndex at the start of a TLE", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': ".length); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': ".length, undefined); assert.deepStrictEqual(pc.tleInfo!.tleValue, undefined); }); test("with characterIndex inside of a TLE", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(21); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(21, undefined); const concat: TLE.FunctionCallValue = nonNullValue(TLE.asFunctionCallValue(pc.tleInfo!.tleValue)); assert.deepStrictEqual(concat.nameToken, TLE.Token.createLiteral(2, "concat")); @@ -347,14 +350,14 @@ suite("PositionContext", () => { }); test("with characterIndex after the end of a closed TLE", () => { - let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", "id"); - let pc = dt.getContextFromDocumentCharacterIndex(32); + let dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B')]\" }", fakeId); + let pc = dt.getContextFromDocumentCharacterIndex(32, undefined); assert.deepStrictEqual(pc.tleInfo, undefined); }); test("with characterIndex after the end of an unclosed TLE", () => { - const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[concat('B'".length); + const dt: DeploymentTemplate = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[concat('B'".length, undefined); const b: TLE.StringValue = nonNullValue(TLE.asStringValue(pc.tleInfo!.tleValue)); assert.deepStrictEqual(b.token, TLE.Token.createQuotedString(9, "'B'")); @@ -370,27 +373,27 @@ suite("PositionContext", () => { suite("hoverInfo", () => { test("in non-string json token", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex(0).getHoverInfo(); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex(0, undefined).getHoverInfo(); assert.deepStrictEqual(hoverInfo, undefined); }); test("in property name json token", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex(3).getHoverInfo(); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex(3, undefined).getHoverInfo(); assert.deepStrictEqual(hoverInfo, undefined); }); }); test("in unrecognized function name", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[toads('B'", "id"); - const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[to".length).getHoverInfo(); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[toads('B'", fakeId); + const hoverInfo: HoverInfo | undefined = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[to".length, undefined).getHoverInfo(); assert.deepStrictEqual(hoverInfo, undefined); }); test("in recognized function name", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", "id"); - const pc = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[c".length); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[concat('B'", fakeId); + const pc = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[c".length, undefined); const hi: HoverInfo = pc.getHoverInfo()!; assert(hi); assert.deepStrictEqual(hi.usage, "concat(arg1, arg2, arg3, ...)"); @@ -398,15 +401,15 @@ suite("PositionContext", () => { }); test("in unrecognized parameter reference", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters('".length); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters('".length, undefined); const hi: HoverInfo | undefined = pc.getHoverInfo(); assert.deepStrictEqual(hi, undefined); }); test("in recognized parameter reference name", () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'pName': { 'type': 'integer' } }, 'a': 'A', 'b': \"[parameters('pName')\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': { 'type': 'integer' } }, 'a': 'A', 'b': \"[parameters('pN".length); + const dt = new DeploymentTemplate("{ 'parameters': { 'pName': { 'type': 'integer' } }, 'a': 'A', 'b': \"[parameters('pName')\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': { 'type': 'integer' } }, 'a': 'A', 'b': \"[parameters('pN".length, undefined); const hi: HoverInfo = pc.getHoverInfo()!; assert(hi); assert.deepStrictEqual(`**pName**${os.EOL}*(parameter)*`, hi.getHoverText()); @@ -414,29 +417,29 @@ suite("PositionContext", () => { }); test("in parameter reference function with empty string parameter", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters('')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters('".length); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters('')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters('".length, undefined); const hi: HoverInfo | undefined = pc.getHoverInfo(); assert.deepStrictEqual(hi, undefined); }); test("in parameter reference function with no arguments", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters()]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters(".length); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[parameters()]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[parameters(".length, undefined); const hi: HoverInfo | undefined = pc.getHoverInfo(); assert.deepStrictEqual(hi, undefined); }); test("in unrecognized variable reference", () => { - const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[variables('B')]\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[variables('".length); + const dt = new DeploymentTemplate("{ 'a': 'A', 'b': \"[variables('B')]\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': 'A', 'b': \"[variables('".length, undefined); const hi: HoverInfo | undefined = pc.getHoverInfo(); assert.deepStrictEqual(hi, undefined); }); test("in recognized variable reference name", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'vName': 3 }, 'a': 'A', 'b': \"[variables('vName')\" }", "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': 3 }, 'a': 'A', 'b': \"[variables('vNam".length); + const dt = new DeploymentTemplate("{ 'variables': { 'vName': 3 }, 'a': 'A', 'b': \"[variables('vName')\" }", fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': 3 }, 'a': 'A', 'b': \"[variables('vNam".length, undefined); const hi: HoverInfo | undefined = pc.getHoverInfo()!; assert(hi); assert.deepStrictEqual(`**vName**${os.EOL}*(variable)*`, hi.getHoverText()); @@ -454,8 +457,8 @@ suite("PositionContext", () => { let keepInClosureForEasierDebugging = testName; keepInClosureForEasierDebugging = keepInClosureForEasierDebugging; - const dt = new DeploymentTemplate(documentText, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(index); + const dt = new DeploymentTemplate(documentText, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(index, undefined); let completionItems: Completion.Item[] = pc.getCompletionItems(); const completionItems2: Completion.Item[] = pc.getCompletionItems(); @@ -466,19 +469,19 @@ suite("PositionContext", () => { } function compareTestableCompletionItems(actualItems: Completion.Item[], expectedItems: Completion.Item[]): void { - let isFunctionCompletions = expectedItems.some(item => allTestDataCompletionNames.has(item.name)); + let isFunctionCompletions = expectedItems.some(item => allTestDataCompletionNames.has(item.label)); // Ignore functions that aren't in our testing list if (isFunctionCompletions) { // Unless it's an empty list - then we want to ensure the actual list is empty, too if (expectedItems.length > 0) { - actualItems = actualItems.filter(item => allTestDataCompletionNames.has(item.name)); + actualItems = actualItems.filter(item => allTestDataCompletionNames.has(item.label)); } } // Make it easier to see missing names quickly - let actualNames = actualItems.map(item => item.name); - let expectedNames = expectedItems.map(item => typeof item === 'string' ? item : item.name); + let actualNames = actualItems.map(item => item.label); + let expectedNames = expectedItems.map(item => typeof item === 'string' ? item : item.label); assert.deepStrictEqual(actualNames, expectedNames, "Names in the completion items did not match"); assert.deepStrictEqual(actualItems, expectedItems); @@ -1138,29 +1141,29 @@ suite("PositionContext", () => { suite("signatureHelp", () => { test("not in a TLE", () => { - const dt = new DeploymentTemplate(`{ "a": "AA" }`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "A`.length); + const dt = new DeploymentTemplate(`{ "a": "AA" }`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "A`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); assert.deepStrictEqual(functionSignatureHelp, undefined); }); test("in empty TLE", () => { - const dt = new DeploymentTemplate(`{ "a": "[]" }`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[`.length); + const dt = new DeploymentTemplate(`{ "a": "[]" }`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); assert.deepStrictEqual(functionSignatureHelp, undefined); }); test("in TLE function name", () => { - const dt = new DeploymentTemplate(`{ "a": "[con]" }`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[con`.length); + const dt = new DeploymentTemplate(`{ "a": "[con]" }`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[con`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); assert.deepStrictEqual(functionSignatureHelp, undefined); }); test("after left parenthesis", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat(`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat(`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat(`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat(`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 0); @@ -1169,8 +1172,8 @@ suite("PositionContext", () => { }); test("inside first parameter", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat('test`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('test`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat('test`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('test`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 0); @@ -1179,8 +1182,8 @@ suite("PositionContext", () => { }); test("inside second parameter", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat('t1', 't2`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('t1', 't2`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat('t1', 't2`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('t1', 't2`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 1); @@ -1189,8 +1192,8 @@ suite("PositionContext", () => { }); test("inside empty parameter", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat(,,,`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat(,,`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat(,,,`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat(,,`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 2); @@ -1199,8 +1202,8 @@ suite("PositionContext", () => { }); test("in variadic parameter when function signature has '...' parameter and the current argument is greater than the parameter count", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f'`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f'`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f'`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f'`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 3); @@ -1209,8 +1212,8 @@ suite("PositionContext", () => { }); test("in variadic parameter when function signature has '...' parameter and the current argument is equal to the parameter count", () => { - const dt = new DeploymentTemplate(`{ "a": "[concat('a', 'b', 'c', 'd'`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd'`.length); + const dt = new DeploymentTemplate(`{ "a": "[concat('a', 'b', 'c', 'd'`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd'`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 3); @@ -1219,8 +1222,8 @@ suite("PositionContext", () => { }); test("in variadic parameter when function signature has 'name...' parameter", () => { - const dt = new DeploymentTemplate(`{ "a": "[resourceId('a', 'b', 'c', 'd', 'e', 'f', 'g'`, "id"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f', 'g'`.length); + const dt = new DeploymentTemplate(`{ "a": "[resourceId('a', 'b', 'c', 'd', 'e', 'f', 'g'`, fakeId); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(`{ "a": "[concat('a', 'b', 'c', 'd', 'e', 'f', 'g'`.length, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp = pc.getSignatureHelp()!; assert(functionSignatureHelp); assert.deepStrictEqual(functionSignatureHelp.activeParameterIndex, 4); @@ -1342,7 +1345,7 @@ suite("PositionContext", () => { const { dt, markers: { bang } } = await parseTemplateWithMarkers(templateString); assert(bang, "You must place a bang ('!') in the expression string to indicate position"); - const pc: PositionContext = dt.getContextFromDocumentCharacterIndex(bang.index); + const pc: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex(bang.index, undefined); const functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); assert.deepStrictEqual(functionSignatureHelp, expected); } @@ -1450,8 +1453,8 @@ suite("PositionContext", () => { }); suite("parameterDefinition", () => { - async function getParameterDefinitionIfAtReference(pc: PositionContext): Promise { - const refInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(); + async function getParameterDefinitionIfAtReference(pc: TemplatePositionContext): Promise { + const refInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(false); if (refInfo && refInfo.definition.definitionKind === "Parameter") { return refInfo.definition; } @@ -1460,20 +1463,20 @@ suite("PositionContext", () => { } test("with no parameters property", async () => { - const dt = new DeploymentTemplate("{ 'a': '[parameters(\"pName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': '[parameters(\"pN".length); + const dt = new DeploymentTemplate("{ 'a': '[parameters(\"pName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': '[parameters(\"pN".length, undefined); assert.deepStrictEqual(await getParameterDefinitionIfAtReference(context), undefined); }); test("with empty parameters property value", async () => { - const dt = new DeploymentTemplate("{ 'parameters': {}, 'a': '[parameters(\"pName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': {}, 'a': '[parameters(\"pN".length); + const dt = new DeploymentTemplate("{ 'parameters': {}, 'a': '[parameters(\"pName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': {}, 'a': '[parameters(\"pN".length, undefined); assert.deepStrictEqual(await getParameterDefinitionIfAtReference(context), undefined); }); test("with matching parameter definition", async () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pNa".length); + const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pNa".length, undefined); const parameterDefinition: IParameterDefinition = nonNullValue(await getParameterDefinitionIfAtReference(context)); assert.deepStrictEqual(parameterDefinition.nameValue.toString(), "pName"); assert.deepStrictEqual(parameterDefinition.description, undefined); @@ -1481,8 +1484,8 @@ suite("PositionContext", () => { }); test("with cursor before parameter name start quote with matching parameter definition", async () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(".length); + const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(".length, undefined); const parameterDefinition: IParameterDefinition = nonNullValue(await getParameterDefinitionIfAtReference(context)); assert.deepStrictEqual(parameterDefinition.nameValue.toString(), "pName"); assert.deepStrictEqual(parameterDefinition.description, undefined); @@ -1490,8 +1493,8 @@ suite("PositionContext", () => { }); test("with cursor after parameter name end quote with matching parameter definition", async () => { - const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\"".length); + const dt = new DeploymentTemplate("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'parameters': { 'pName': {} }, 'a': '[parameters(\"pName\"".length, undefined); const parameterDefinition: IParameterDefinition = nonNullValue(await getParameterDefinitionIfAtReference(context)); assert.deepStrictEqual(parameterDefinition.nameValue.toString(), "pName"); assert.deepStrictEqual(parameterDefinition.description, undefined); @@ -1500,8 +1503,8 @@ suite("PositionContext", () => { }); suite("variableDefinition", () => { - function getVariableDefinitionIfAtReference(pc: PositionContext): IVariableDefinition | undefined { - const refInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(); + function getVariableDefinitionIfAtReference(pc: TemplatePositionContext): IVariableDefinition | undefined { + const refInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(false); if (refInfo && isVariableDefinition(refInfo.definition)) { return refInfo.definition; } @@ -1510,36 +1513,36 @@ suite("PositionContext", () => { } test("with no variables property", () => { - const dt = new DeploymentTemplate("{ 'a': '[variables(\"vName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': '[variables(\"vN".length); + const dt = new DeploymentTemplate("{ 'a': '[variables(\"vName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'a': '[variables(\"vN".length, undefined); assert.deepStrictEqual(getVariableDefinitionIfAtReference(context), undefined); }); test("with empty variables property value", () => { - const dt = new DeploymentTemplate("{ 'variables': {}, 'a': '[variables(\"vName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': {}, 'a': '[variables(\"vN".length); + const dt = new DeploymentTemplate("{ 'variables': {}, 'a': '[variables(\"vName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': {}, 'a': '[variables(\"vN".length, undefined); assert.deepStrictEqual(getVariableDefinitionIfAtReference(context), undefined); }); test("with matching variable definition", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vNa".length); + const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vNa".length, undefined); const vDef: IVariableDefinition = nonNullValue(getVariableDefinitionIfAtReference(context)); assert.deepStrictEqual(vDef.nameValue.toString(), "vName"); assert.deepStrictEqual(vDef.span, new Language.Span(17, 11)); }); test("with cursor before variable name start quote with matching variable definition", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(".length); + const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(".length, undefined); const vDef: IVariableDefinition = nonNullValue(getVariableDefinitionIfAtReference(context)); assert.deepStrictEqual(vDef.nameValue.toString(), "vName"); assert.deepStrictEqual(vDef.span, new Language.Span(17, 11)); }); test("with cursor after parameter name end quote with matching parameter definition", () => { - const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", "id"); - const context: PositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\"".length); + const dt = new DeploymentTemplate("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\")]' }", fakeId); + const context: TemplatePositionContext = dt.getContextFromDocumentCharacterIndex("{ 'variables': { 'vName': {} }, 'a': '[variables(\"vName\"".length, undefined); const vDef: IVariableDefinition = nonNullValue(getVariableDefinitionIfAtReference(context)); assert.deepStrictEqual(vDef.nameValue.toString(), "vName"); assert.deepStrictEqual(vDef.span, new Language.Span(17, 11)); diff --git a/test/TemplateTests.test.ts b/test/TemplateTests.test.ts index a3123a5ab..d538bf589 100644 --- a/test/TemplateTests.test.ts +++ b/test/TemplateTests.test.ts @@ -5,6 +5,7 @@ // tslint:disable:no-unused-expression max-func-body-length max-line-length object-literal-key-quotes import * as assert from "assert"; +import { Uri } from "vscode"; import { DeploymentTemplate } from "../extension.bundle"; import { stringify } from "./support/stringify"; import { useRealFunctionMetadata, useTestFunctionMetadata } from "./TestData"; @@ -18,10 +19,10 @@ suite("Template tests", () => { async function verifyTemplateHasNoErrors(template: string | object): Promise { useRealFunctionMetadata(); try { - const dt = new DeploymentTemplate(typeof template === "string" ? template : stringify(template), "id"); + const dt = new DeploymentTemplate(typeof template === "string" ? template : stringify(template), Uri.file("id")); const expectedErrors: string[] = [ ]; - let errors = await dt.errorsPromise; + let errors = await dt.getErrors(undefined); assert.deepStrictEqual(errors, expectedErrors, "Expected no errors in template"); } finally { useTestFunctionMetadata(); diff --git a/test/TestData.ts b/test/TestData.ts index 016ea8239..05890d161 100644 --- a/test/TestData.ts +++ b/test/TestData.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import { ITest, ITestCallbackContext } from 'mocha'; import * as path from 'path'; import { AzureRMAssets, Completion, Language } from "../extension.bundle"; -import { armTest } from './support/armTest'; +import { ITestPreparation, ITestPreparationResult, testWithPrep } from './support/testWithPrep'; // By default we use the test metadata for tests export function useTestFunctionMetadata(): void { @@ -20,6 +20,17 @@ export function useRealFunctionMetadata(): void { console.log("Re-installing real function metadata"); } +export class UseRealFunctionMetadata implements ITestPreparation { + public static readonly instance: UseRealFunctionMetadata = new UseRealFunctionMetadata(); + + public pretest(this: ITestCallbackContext): ITestPreparationResult { + useRealFunctionMetadata(); + return { + postTest: useTestFunctionMetadata + }; + } +} + export async function runWithRealFunctionMetadata(callback: () => Promise): Promise { try { useRealFunctionMetadata(); @@ -31,15 +42,10 @@ export async function runWithRealFunctionMetadata(callback: () => Promise Promise): ITest { - return armTest( - expectation, - { - useRealFunctionMetadata: true - }, - callback); + return testWithPrep(expectation, [UseRealFunctionMetadata.instance], callback); } -export const allTestDataCompletionNames = new Set(allTestDataExpectedCompletions(0, 0).map(item => item.name)); +export const allTestDataCompletionNames = new Set(allTestDataExpectedCompletions(0, 0).map(item => item.label)); export function allTestDataExpectedCompletions(startIndex: number, length: number): Completion.Item[] { return [ @@ -81,149 +87,149 @@ export function allTestDataExpectedCompletions(startIndex: number, length: numbe } export function expectedAddCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("add", "add($0)", new Language.Span(startIndex, length), "(function) add(operand1, operand2)", "Returns the sum of the two provided integers.", Completion.CompletionKind.Function); + return new Completion.Item("add", "add", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) add(operand1, operand2)", "Returns the sum of the two provided integers."); } export function expectedBase64Completion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("base64", "base64($0)", new Language.Span(startIndex, length), "(function) base64(inputString)", "Returns the base64 representation of the input string.", Completion.CompletionKind.Function); + return new Completion.Item("base64", "base64", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) base64(inputString)", "Returns the base64 representation of the input string."); } export function expectedConcatCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("concat", "concat($0)", new Language.Span(startIndex, length), "(function) concat(arg1, arg2, arg3, ...)", "Combines multiple values and returns the concatenated result. This function can take any number of arguments, and can accept either strings or arrays for the parameters.", Completion.CompletionKind.Function); + return new Completion.Item("concat", "concat", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) concat(arg1, arg2, arg3, ...)", "Combines multiple values and returns the concatenated result. This function can take any number of arguments, and can accept either strings or arrays for the parameters."); } export function expectedCopyIndexCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("copyIndex", "copyIndex($0)", new Language.Span(startIndex, length), "(function) copyIndex([offset]) or copyIndex(loopName, [offset])", "Returns the current index of an iteration loop.\nThis function is always used with a copy object.", Completion.CompletionKind.Function); + return new Completion.Item("copyIndex", "copyIndex", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) copyIndex([offset]) or copyIndex(loopName, [offset])", "Returns the current index of an iteration loop.\nThis function is always used with a copy object."); } export function expectedDeploymentCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("deployment", "deployment()$0", new Language.Span(startIndex, length), "(function) deployment() [object]", "Returns information about the current deployment operation. This function returns the object that is passed during deployment. The properties in the returned object will differ based on whether the deployment object is passed as a link or as an in-line object.", Completion.CompletionKind.Function); + return new Completion.Item("deployment", "deployment", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) deployment() [object]", "Returns information about the current deployment operation. This function returns the object that is passed during deployment. The properties in the returned object will differ based on whether the deployment object is passed as a link or as an in-line object."); } export function expectedDivCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("div", "div($0)", new Language.Span(startIndex, length), "(function) div(operand1, operand2)", "Returns the integer division of the two provided integers.", Completion.CompletionKind.Function); + return new Completion.Item("div", "div", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) div(operand1, operand2)", "Returns the integer division of the two provided integers."); } export function expectedEqualsCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("equals", "equals($0)", new Language.Span(startIndex, length), "(function) equals(arg1, arg2)", "Checks whether two values equal each other.", Completion.CompletionKind.Function); + return new Completion.Item("equals", "equals", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) equals(arg1, arg2)", "Checks whether two values equal each other."); } export function expectedIntCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("int", "int($0)", new Language.Span(startIndex, length), "(function) int(valueToConvert)", "Converts the specified value to Integer.", Completion.CompletionKind.Function); + return new Completion.Item("int", "int", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) int(valueToConvert)", "Converts the specified value to Integer."); } export function expectedLengthCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("length", "length($0)", new Language.Span(startIndex, length), "(function) length(array/string)", "Returns the number of elements in an array or the number of characters in a string. You can use this function with an array to specify the number of iterations when creating resources.", Completion.CompletionKind.Function); + return new Completion.Item("length", "length", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) length(array/string)", "Returns the number of elements in an array or the number of characters in a string. You can use this function with an array to specify the number of iterations when creating resources."); } export function expectedListKeysCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("listKeys", "listKeys($0)", new Language.Span(startIndex, length), "(function) listKeys(resourceName/resourceIdentifier, apiVersion) [object]", "Returns the keys of a storage account. The resourceId can be specified by using the resourceId function or by using the format providerNamespace/resourceType/resourceName. You can use the function to get the primary (key[0]) and secondary key (key[1]).", Completion.CompletionKind.Function); + return new Completion.Item("listKeys", "listKeys", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) listKeys(resourceName/resourceIdentifier, apiVersion) [object]", "Returns the keys of a storage account. The resourceId can be specified by using the resourceId function or by using the format providerNamespace/resourceType/resourceName. You can use the function to get the primary (key[0]) and secondary key (key[1])."); } export function expectedListPackageCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("listPackage", "listPackage($0)", new Language.Span(startIndex, length), "(function) listPackage(resourceName\/resourceIdentifier, apiVersion)", "Lists the virtual network gateway package. The resourceId can be specified by using the resourceId function or by using the format providerNamespace/resourceType/resourceName.", Completion.CompletionKind.Function); + return new Completion.Item("listPackage", "listPackage", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) listPackage(resourceName\/resourceIdentifier, apiVersion)", "Lists the virtual network gateway package. The resourceId can be specified by using the resourceId function or by using the format providerNamespace/resourceType/resourceName."); } export function expectedModCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("mod", "mod($0)", new Language.Span(startIndex, length), "(function) mod(operand1, operand2)", "Returns the remainder of the integer division using the two provided integers.", Completion.CompletionKind.Function); + return new Completion.Item("mod", "mod", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) mod(operand1, operand2)", "Returns the remainder of the integer division using the two provided integers."); } export function expectedMulCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("mul", "mul($0)", new Language.Span(startIndex, length), "(function) mul(operand1, operand2)", "Returns the multiplication of the two provided integers.", Completion.CompletionKind.Function); + return new Completion.Item("mul", "mul", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) mul(operand1, operand2)", "Returns the multiplication of the two provided integers."); } export function expectedPadLeftCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("padLeft", "padLeft($0)", new Language.Span(startIndex, length), "(function) padLeft(stringToPad, totalLength, paddingCharacter)", "Returns a right-aligned string by adding characters to the left until reaching the total specified length.", Completion.CompletionKind.Function); + return new Completion.Item("padLeft", "padLeft", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) padLeft(stringToPad, totalLength, paddingCharacter)", "Returns a right-aligned string by adding characters to the left until reaching the total specified length."); } export function expectedParametersCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("parameters", "parameters($0)", new Language.Span(startIndex, length), "(function) parameters(parameterName)", "Returns a parameter value. The specified parameter name must be defined in the parameters section of the template.", Completion.CompletionKind.Function); + return new Completion.Item("parameters", "parameters", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) parameters(parameterName)", "Returns a parameter value. The specified parameter name must be defined in the parameters section of the template."); } export function expectedProvidersCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("providers", "providers($0)", new Language.Span(startIndex, length), "(function) providers(providerNamespace, [resourceType])", "Return information about a resource provider and its supported resource types. If not type is provided, all of the supported types are returned.", Completion.CompletionKind.Function); + return new Completion.Item("providers", "providers", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) providers(providerNamespace, [resourceType])", "Return information about a resource provider and its supported resource types. If not type is provided, all of the supported types are returned."); } export function expectedReferenceCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("reference", "reference($0)", new Language.Span(startIndex, length), "(function) reference(resourceName/resourceIdentifier, [apiVersion], ['Full'])", "Enables an expression to derive its value from another resource's runtime state.", Completion.CompletionKind.Function); + return new Completion.Item("reference", "reference", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) reference(resourceName/resourceIdentifier, [apiVersion], ['Full'])", "Enables an expression to derive its value from another resource's runtime state."); } export function expectedReplaceCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("replace", "replace($0)", new Language.Span(startIndex, length), "(function) replace(originalString, oldCharacter, newCharacter)", "Returns a new string with all instances of one character in the specified string replaced by another character.", Completion.CompletionKind.Function); + return new Completion.Item("replace", "replace", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) replace(originalString, oldCharacter, newCharacter)", "Returns a new string with all instances of one character in the specified string replaced by another character."); } export function expectedResourceGroupCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("resourceGroup", "resourceGroup()$0", new Language.Span(startIndex, length), "(function) resourceGroup() [object]", "Returns a structured object that represents the current resource group.", Completion.CompletionKind.Function); + return new Completion.Item("resourceGroup", "resourceGroup", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) resourceGroup() [object]", "Returns a structured object that represents the current resource group."); } export function expectedResourceIdCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("resourceId", "resourceId($0)", new Language.Span(startIndex, length), "(function) resourceId([subscriptionId], [resourceGroupName], resourceType, resourceName1, [resourceName2]...)", "Returns the unique identifier of a resource. You use this function when the resource name is ambiguous or not provisioned within the same template.", Completion.CompletionKind.Function); + return new Completion.Item("resourceId", "resourceId", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) resourceId([subscriptionId], [resourceGroupName], resourceType, resourceName1, [resourceName2]...)", "Returns the unique identifier of a resource. You use this function when the resource name is ambiguous or not provisioned within the same template."); } export function expectedSkipCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("skip", "skip($0)", new Language.Span(startIndex, length), "(function) skip(originalValue, numberToSkip)", "Returns an array or string with all of the elements or characters after the specified number in the array or string.", Completion.CompletionKind.Function); + return new Completion.Item("skip", "skip", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) skip(originalValue, numberToSkip)", "Returns an array or string with all of the elements or characters after the specified number in the array or string."); } export function expectedSplitCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("split", "split($0)", new Language.Span(startIndex, length), "(function) split(inputString, delimiter)", "Returns an array of strings that contains the substrings of the input string that are delimited by the sent delimiters.", Completion.CompletionKind.Function); + return new Completion.Item("split", "split", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) split(inputString, delimiter)", "Returns an array of strings that contains the substrings of the input string that are delimited by the sent delimiters."); } export function expectedStringCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("string", "string($0)", new Language.Span(startIndex, length), "(function) string(valueToConvert)", "Converts the specified value to String.", Completion.CompletionKind.Function); + return new Completion.Item("string", "string", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) string(valueToConvert)", "Converts the specified value to String."); } export function expectedSubCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("sub", "sub($0)", new Language.Span(startIndex, length), "(function) sub(operand1, operand2)", "Returns the subtraction of the two provided integers.", Completion.CompletionKind.Function); + return new Completion.Item("sub", "sub", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) sub(operand1, operand2)", "Returns the subtraction of the two provided integers."); } export function expectedSubscriptionCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("subscription", "subscription()$0", new Language.Span(startIndex, length), "(function) subscription() [object]", "Returns details about the subscription.", Completion.CompletionKind.Function); + return new Completion.Item("subscription", "subscription", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) subscription() [object]", "Returns details about the subscription."); } export function expectedSubscriptionResourceIdCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("subscriptionResourceId", "subscriptionResourceId($0)", new Language.Span(startIndex, length), "(function) subscriptionResourceId([subscriptionId], resourceType, resourceName1, [resourceName2]...)", "Returns the unique resource identifier of a subscription scoped resource. You use this function to create a resourceId for a given resource as required by a property value.", Completion.CompletionKind.Function); + return new Completion.Item("subscriptionResourceId", "subscriptionResourceId", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) subscriptionResourceId([subscriptionId], resourceType, resourceName1, [resourceName2]...)", "Returns the unique resource identifier of a subscription scoped resource. You use this function to create a resourceId for a given resource as required by a property value."); } export function expectedSubstringCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("substring", "substring($0)", new Language.Span(startIndex, length), "(function) substring(stringToParse, startIndex, length)", "Returns a substring that starts at the specified character position and contains the specified number of characters.", Completion.CompletionKind.Function); + return new Completion.Item("substring", "substring", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) substring(stringToParse, startIndex, length)", "Returns a substring that starts at the specified character position and contains the specified number of characters."); } export function expectedTakeCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("take", "take($0)", new Language.Span(startIndex, length), "(function) take(originalValue, numberToTake)", "Returns an array or string with the specified number of elements or characters from the start of the array or string.", Completion.CompletionKind.Function); + return new Completion.Item("take", "take", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) take(originalValue, numberToTake)", "Returns an array or string with the specified number of elements or characters from the start of the array or string."); } export function expectedToLowerCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("toLower", "toLower($0)", new Language.Span(startIndex, length), "(function) toLower(string)", "Converts the specified string to lower case.", Completion.CompletionKind.Function); + return new Completion.Item("toLower", "toLower", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) toLower(string)", "Converts the specified string to lower case."); } export function expectedToUpperCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("toUpper", "toUpper($0)", new Language.Span(startIndex, length), "(function) toUpper(string)", "Converts the specified string to upper case.", Completion.CompletionKind.Function); + return new Completion.Item("toUpper", "toUpper", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) toUpper(string)", "Converts the specified string to upper case."); } export function expectedTrimCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("trim", "trim($0)", new Language.Span(startIndex, length), "(function) trim(stringToTrim)", "Removes all leading and trailing white-space characters from the specified string.", Completion.CompletionKind.Function); + return new Completion.Item("trim", "trim", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) trim(stringToTrim)", "Removes all leading and trailing white-space characters from the specified string."); } export function expectedUniqueStringCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("uniqueString", "uniqueString($0)", new Language.Span(startIndex, length), "(function) uniqueString(stringForCreatingUniqueString, ...)", "Performs a 64-bit hash of the provided strings to create a unique string. This function is helpful when you need to create a unique name for a resource. You provide parameter values that represent the level of uniqueness for the result. You can specify whether the name is unique for your subscription, resource group, or deployment.", Completion.CompletionKind.Function); + return new Completion.Item("uniqueString", "uniqueString", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) uniqueString(stringForCreatingUniqueString, ...)", "Performs a 64-bit hash of the provided strings to create a unique string. This function is helpful when you need to create a unique name for a resource. You provide parameter values that represent the level of uniqueness for the result. You can specify whether the name is unique for your subscription, resource group, or deployment."); } export function expectedUriCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("uri", "uri($0)", new Language.Span(startIndex, length), "(function) uri(baseUri, relativeUri)", "Creates an absolute URI by combining the baseUri and the relativeUri string.", Completion.CompletionKind.Function); + return new Completion.Item("uri", "uri", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) uri(baseUri, relativeUri)", "Creates an absolute URI by combining the baseUri and the relativeUri string."); } export function expectedVariablesCompletion(startIndex: number, length: number): Completion.Item { - return new Completion.Item("variables", "variables($0)", new Language.Span(startIndex, length), "(function) variables(variableName)", "Returns the value of variable. The specified variable name must be defined in the variables section of the template.", Completion.CompletionKind.Function); + return new Completion.Item("variables", "variables", new Language.Span(startIndex, length), Completion.CompletionKind.Function, "(function) variables(variableName)", "Returns the value of variable. The specified variable name must be defined in the variables section of the template."); } export function parameterCompletion(parameterName: string, startIndex: number, length: number, includeRightParenthesis: boolean = true): Completion.Item { - return new Completion.Item(`'${parameterName}'`, `'${parameterName}'${includeRightParenthesis ? ")" : ""}$0`, new Language.Span(startIndex, length), "(parameter)", undefined, Completion.CompletionKind.Parameter); + return new Completion.Item(`'${parameterName}'`, `'${parameterName}'${includeRightParenthesis ? ")" : ""}`, new Language.Span(startIndex, length), Completion.CompletionKind.Parameter, "(parameter)", undefined); } export function propertyCompletion(propertyName: string, startIndex: number, length: number): Completion.Item { - return new Completion.Item(propertyName, `${propertyName}$0`, new Language.Span(startIndex, length), "(property)", "", Completion.CompletionKind.Property); + return new Completion.Item(propertyName, `${propertyName}`, new Language.Span(startIndex, length), Completion.CompletionKind.Property, "(property)"); } export function variableCompletion(variableName: string, startIndex: number, length: number, includeRightParenthesis: boolean = true): Completion.Item { - return new Completion.Item(`'${variableName}'`, `'${variableName}'${includeRightParenthesis ? ")" : ""}$0`, new Language.Span(startIndex, length), "(variable)", "", Completion.CompletionKind.Variable); + return new Completion.Item(`'${variableName}'`, `'${variableName}'${includeRightParenthesis ? ")" : ""}`, new Language.Span(startIndex, length), Completion.CompletionKind.Variable, "(variable)"); } diff --git a/test/UserFunctions.test.ts b/test/UserFunctions.test.ts index 8cdf4f90c..b1b7c59bd 100644 --- a/test/UserFunctions.test.ts +++ b/test/UserFunctions.test.ts @@ -1305,7 +1305,7 @@ suite("User functions", () => { expectedHoverText: string, expectedSpan?: Language.Span ): Promise { - const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex); + const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex, undefined); let hoverInfo: HoverInfo = pc.getHoverInfo()!; assert(hoverInfo, "Expected non-empty hover info"); hoverInfo = hoverInfo!; @@ -1360,8 +1360,8 @@ suite("User functions", () => { expectedReferenceKind: DefinitionKind, expectedDefinitionStart: number ): Promise { - const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex); - const refInfo: IReferenceSite = pc.getReferenceSiteInfo()!; + const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex, undefined); + const refInfo: IReferenceSite = pc.getReferenceSiteInfo(false)!; assert(refInfo, "Expected non-null IReferenceSite"); assert.deepStrictEqual(refInfo.definition.definitionKind, expectedReferenceKind); @@ -1776,17 +1776,17 @@ suite("User functions", () => { } }; - const allBuiltinsExpectedCompletions = allTestDataExpectedCompletions(0, 0).map(c => <[string, string]>[c.name, c.insertText]); - const allNamespaceExpectedCompletions: [string, string][] = [["mixedCaseNamespace", "mixedCaseNamespace.$0"], ["udf", "udf.$0"]]; + const allBuiltinsExpectedCompletions = allTestDataExpectedCompletions(0, 0).map(c => <[string, string]>[c.label, c.insertText]); + const allNamespaceExpectedCompletions: [string, string][] = [["mixedCaseNamespace", "mixedCaseNamespace"], ["udf", "udf"]]; const allUdfNsFunctionsCompletions: [string, string][] = [ - ["udf.mixedCaseFunc", "mixedCaseFunc()$0"], - ["udf.string", "string($0)"], - ["udf.parameters", "parameters($0)"], - ["udf.udf", "udf($0)"], - ["udf.udf2", "udf2()$0"], - ["udf.udf3", "udf3()$0"], - ["udf.udf34", "udf34()$0"]]; - const allMixedCaseNsFunctionsCompletions: [string, string][] = [["mixedCaseNamespace.howdy", "howdy()$0"]]; + ["udf.mixedCaseFunc", "mixedCaseFunc"], + ["udf.string", "string"], + ["udf.parameters", "parameters"], + ["udf.udf", "udf"], + ["udf.udf2", "udf2"], + ["udf.udf3", "udf3"], + ["udf.udf34", "udf34"]]; + const allMixedCaseNsFunctionsCompletions: [string, string][] = [["mixedCaseNamespace.howdy", "howdy"]]; suite("Completing UDF function names", () => { suite("Completing udf.xxx gives udf's functions starting with xxx - not found", () => { @@ -1797,27 +1797,27 @@ suite("User functions", () => { suite("Completing inside xxx in udf.xxx gives only udf's functions starting with xxx", () => { // $0 indicates where the cursor should be placed after replacement - createCompletionsTest(userFuncsTemplate2, '', 'udf.p!', [["udf.parameters", "parameters($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.u!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.ud!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.udf!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.udf2!', [["udf.udf2", "udf2()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.udf3!', [["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.udf34!', [["udf.udf34", "udf34()$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.p!', [["udf.parameters", "parameters"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.u!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.ud!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.udf!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.udf2!', [["udf.udf2", "udf2"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.udf3!', [["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.udf34!', [["udf.udf34", "udf34"]]); }); suite("Completing udf.xxx gives udf's functions starting with xxx - case insensitive", () => { - createCompletionsTest(userFuncsTemplate2, '', 'udf.P!', [["udf.parameters", "parameters($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.U!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.uD!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.udF!', [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf.MIXEDCase!', [["udf.mixedCaseFunc", "mixedCaseFunc()$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.P!', [["udf.parameters", "parameters"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.U!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.uD!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.udF!', [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.MIXEDCase!', [["udf.mixedCaseFunc", "mixedCaseFunc"]]); }); suite("Completing built-in functions inside functions", () => { - createCompletionsTest(userFuncsTemplate2, '', 'param!', [["parameters", "parameters($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'p!', [["padLeft", "padLeft($0)"], ["parameters", "parameters($0)"], ["providers", "providers($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'P!', [["padLeft", "padLeft($0)"], ["parameters", "parameters($0)"], ["providers", "providers($0)"]]); + createCompletionsTest(userFuncsTemplate2, '', 'param!', [["parameters", "parameters"]]); + createCompletionsTest(userFuncsTemplate2, '', 'p!', [["padLeft", "padLeft"], ["parameters", "parameters"], ["providers", "providers"]]); + createCompletionsTest(userFuncsTemplate2, '', 'P!', [["padLeft", "padLeft"], ["parameters", "parameters"], ["providers", "providers"]]); }); suite("Completing built-in functions with UDF function names returns empty", () => { @@ -1825,7 +1825,7 @@ suite("User functions", () => { }); suite("Completing udf.param does not find built-in parameters function", () => { - createCompletionsTest(userFuncsTemplate2, '', 'udf.param!', [["udf.parameters", "parameters($0)"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf.param!', [["udf.parameters", "parameters"]]); }); suite("Completing udf. gives udf's functions", () => { @@ -1842,10 +1842,10 @@ suite("User functions", () => { }); suite("Completing in middle of function name", () => { - createCompletionsTest(userFuncsTemplate2, "", "udf.!udf34", [["udf.mixedCaseFunc", "mixedCaseFunc()$0"], ["udf.string", "string($0)"], ["udf.parameters", "parameters($0)"], ["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, "", "udf.u!df34", [["udf.udf", "udf($0)"], ["udf.udf2", "udf2()$0"], ["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, "", "udf.udf3!4", [["udf.udf3", "udf3()$0"], ["udf.udf34", "udf34()$0"]]); - createCompletionsTest(userFuncsTemplate2, "", "udf.udf34!", [["udf.udf34", "udf34()$0"]]); + createCompletionsTest(userFuncsTemplate2, "", "udf.!udf34", [["udf.mixedCaseFunc", "mixedCaseFunc"], ["udf.string", "string"], ["udf.parameters", "parameters"], ["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, "", "udf.u!df34", [["udf.udf", "udf"], ["udf.udf2", "udf2"], ["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, "", "udf.udf3!4", [["udf.udf3", "udf3"], ["udf.udf34", "udf34"]]); + createCompletionsTest(userFuncsTemplate2, "", "udf.udf34!", [["udf.udf34", "udf34"]]); createCompletionsTest(userFuncsTemplate2, "", "udf.udf345!", []); }); }); // end Completing UDF function names @@ -1857,21 +1857,21 @@ suite("User functions", () => { }); suite("Only matches namespace", () => { - createCompletionsTest(userFuncsTemplate2, '', 'ud!', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf!', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'mixedCase!', [["mixedCaseNamespace", "mixedCaseNamespace.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'mixedCaseNamespace!', [["mixedCaseNamespace", "mixedCaseNamespace.$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'ud!', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf!', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'mixedCase!', [["mixedCaseNamespace", "mixedCaseNamespace"]]); + createCompletionsTest(userFuncsTemplate2, '', 'mixedCaseNamespace!', [["mixedCaseNamespace", "mixedCaseNamespace"]]); }); suite("Only matches namespace - case insensitive", () => { - createCompletionsTest(userFuncsTemplate2, '', 'ud!', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf!', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'MIXEDCASE!', [["mixedCaseNamespace", "mixedCaseNamespace.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'mixedCASENAMESPACE!', [["mixedCaseNamespace", "mixedCaseNamespace.$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'ud!', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf!', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'MIXEDCASE!', [["mixedCaseNamespace", "mixedCaseNamespace"]]); + createCompletionsTest(userFuncsTemplate2, '', 'mixedCASENAMESPACE!', [["mixedCaseNamespace", "mixedCaseNamespace"]]); }); suite("Matches namespaces and built-in functions", () => { - createCompletionsTest(userFuncsTemplate2, '', 'u!', [["udf", "udf.$0"], ["uniqueString", "uniqueString($0)"], ["uri", "uri($0)"]]); + createCompletionsTest(userFuncsTemplate2, '', 'u!', [["udf", "udf"], ["uniqueString", "uniqueString"], ["uri", "uri"]]); }); }); // end Completing UDF namespaces @@ -1883,18 +1883,18 @@ suite("User functions", () => { suite("Matches namespaces and built-in functions", () => { createCompletionsTest(userFuncsTemplate2, '', '!udf.string', [...allNamespaceExpectedCompletions, ...allBuiltinsExpectedCompletions]); - createCompletionsTest(userFuncsTemplate2, '', 'u!df.string', [["udf", "udf.$0"], ["uniqueString", "uniqueString($0)"], ["uri", "uri($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'ud!f.abc', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'udf!.abc', [["udf", "udf.$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'mixed!Ca.abc', [["mixedCaseNamespace", "mixedCaseNamespace.$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'u!df.string', [["udf", "udf"], ["uniqueString", "uniqueString"], ["uri", "uri"]]); + createCompletionsTest(userFuncsTemplate2, '', 'ud!f.abc', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'udf!.abc', [["udf", "udf"]]); + createCompletionsTest(userFuncsTemplate2, '', 'mixed!Ca.abc', [["mixedCaseNamespace", "mixedCaseNamespace"]]); }); suite("Parameters in outer scope", () => { - createCompletionsTest(userFuncsTemplate2, '', 'parameters!', [["parameters", "parameters($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'parameters(!', [["'year'", "'year')$0"], ["'apiVersion'", "'apiVersion')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'parameters(!)', [["'year'", "'year')$0"], ["'apiVersion'", "'apiVersion')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', "parameters('!y", [["'year'", "'year')$0"], ["'apiVersion'", "'apiVersion')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', "parameters('y!", [["'year'", "'year')$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'parameters!', [["parameters", "parameters"]]); + createCompletionsTest(userFuncsTemplate2, '', 'parameters(!', [["'year'", "'year')"], ["'apiVersion'", "'apiVersion')"]]); + createCompletionsTest(userFuncsTemplate2, '', 'parameters(!)', [["'year'", "'year')"], ["'apiVersion'", "'apiVersion')"]]); + createCompletionsTest(userFuncsTemplate2, '', "parameters('!y", [["'year'", "'year')"], ["'apiVersion'", "'apiVersion')"]]); + createCompletionsTest(userFuncsTemplate2, '', "parameters('y!", [["'year'", "'year')"]]); // Don't complete parameters against UDF with same name createCompletionsTest(userFuncsTemplate2, '', "udf.parameters('y!", []); @@ -1902,19 +1902,19 @@ suite("User functions", () => { suite("Parameters in function scope", () => { // Parameter completions should only be parameters inside the function - createCompletionsTest(userFuncsTemplate2, '', 'parameters!', [["parameters", "parameters($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'parameters(!', [["'year'", "'year')$0"], ["'day'", "'day')$0"], ["'month'", "'month')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', "parameters('y!", [["'year'", "'year')$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'parameters!', [["parameters", "parameters"]]); + createCompletionsTest(userFuncsTemplate2, '', 'parameters(!', [["'year'", "'year')"], ["'day'", "'day')"], ["'month'", "'month')"]]); + createCompletionsTest(userFuncsTemplate2, '', "parameters('y!", [["'year'", "'year')"]]); }); suite("Variables in outer scope", () => { - createCompletionsTest(userFuncsTemplate2, '', 'variables!', [["variables", "variables($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'variables(!', [["'var1'", "'var1')$0"], ["'var2'", "'var2')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', 'variables(!)', [["'var1'", "'var1')$0"], ["'var2'", "'var2')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', "variables('!y", [["'var1'", "'var1')$0"], ["'var2'", "'var2')$0"]]); + createCompletionsTest(userFuncsTemplate2, '', 'variables!', [["variables", "variables"]]); + createCompletionsTest(userFuncsTemplate2, '', 'variables(!', [["'var1'", "'var1')"], ["'var2'", "'var2')"]]); + createCompletionsTest(userFuncsTemplate2, '', 'variables(!)', [["'var1'", "'var1')"], ["'var2'", "'var2')"]]); + createCompletionsTest(userFuncsTemplate2, '', "variables('!y", [["'var1'", "'var1')"], ["'var2'", "'var2')"]]); createCompletionsTest(userFuncsTemplate2, '', "variables('y!", []); - createCompletionsTest(userFuncsTemplate2, '', "variables('v!", [["'var1'", "'var1')$0"], ["'var2'", "'var2')$0"]]); - createCompletionsTest(userFuncsTemplate2, '', "variables('var1!", [["'var1'", "'var1')$0"]]); + createCompletionsTest(userFuncsTemplate2, '', "variables('v!", [["'var1'", "'var1')"], ["'var2'", "'var2')"]]); + createCompletionsTest(userFuncsTemplate2, '', "variables('var1!", [["'var1'", "'var1')"]]); // Don't complete variables against UDF with same name createCompletionsTest(userFuncsTemplate2, '', "udf.variables('var1!", []); @@ -1922,7 +1922,7 @@ suite("User functions", () => { suite("Variables in function scope", () => { // CONSIDER: Ideally this would not return a 'variables' completion at all - createCompletionsTest(userFuncsTemplate2, '', 'variables!', [["variables", "variables($0)"]]); + createCompletionsTest(userFuncsTemplate2, '', 'variables!', [["variables", "variables"]]); // No variables availabe in function scope createCompletionsTest(userFuncsTemplate2, '', 'variables(!', []); @@ -1930,8 +1930,8 @@ suite("User functions", () => { suite("User namespaces and functions not available in function scope", () => { createCompletionsTest(userFuncsTemplate2, '', '!udf.string', [...allBuiltinsExpectedCompletions]); - createCompletionsTest(userFuncsTemplate2, '', 'u!df.string', [["uniqueString", "uniqueString($0)"], ["uri", "uri($0)"]]); - createCompletionsTest(userFuncsTemplate2, '', 'u!', [["uniqueString", "uniqueString($0)"], ["uri", "uri($0)"]]); + createCompletionsTest(userFuncsTemplate2, '', 'u!df.string', [["uniqueString", "uniqueString"], ["uri", "uri"]]); + createCompletionsTest(userFuncsTemplate2, '', 'u!', [["uniqueString", "uniqueString"], ["uri", "uri"]]); createCompletionsTest(userFuncsTemplate2, '', 'udf!.string', []); createCompletionsTest(userFuncsTemplate2, '', 'udf.!', []); }); diff --git a/test/VariableIteration.test.ts b/test/VariableIteration.test.ts index cbaf71763..8fd9e1f55 100644 --- a/test/VariableIteration.test.ts +++ b/test/VariableIteration.test.ts @@ -6,6 +6,7 @@ // tslint:disable:no-non-null-assertion object-literal-key-quotes variable-name no-constant-condition no-any import * as assert from 'assert'; +import { Uri } from 'vscode'; import { DeploymentTemplate, IVariableDefinition, Json } from "../extension.bundle"; import { createCompletionsTest } from './support/createCompletionsTest'; import { IDeploymentTemplate } from "./support/diagnostics"; @@ -13,6 +14,8 @@ import { parseTemplate, parseTemplateWithMarkers } from "./support/parseTemplate import { stringify } from "./support/stringify"; import { testGetReferences } from './support/testGetReferences'; +const fakeId = Uri.file("https://fake-id"); + suite("Variable iteration (copy blocks)", () => { suite("top-level variable copy blocks", () => { @@ -107,7 +110,7 @@ suite("Variable iteration (copy blocks)", () => { }] } }), - "id"); + fakeId); assert(!!dt.topLevelScope.getVariableDefinition('diskNames')); }); @@ -121,7 +124,7 @@ suite("Variable iteration (copy blocks)", () => { }] } }), - "id"); + fakeId); // Right now we still create the variable // CONSIDER: Instead add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) @@ -141,7 +144,7 @@ suite("Variable iteration (copy blocks)", () => { }] } }), - "id"); + fakeId); // Right now we just don't create the variable // CONSIDER: Instead add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) @@ -158,7 +161,7 @@ suite("Variable iteration (copy blocks)", () => { }] } }), - "id"); + fakeId); // Right now we just don't create the variable // CONSIDER: Instead add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) @@ -180,7 +183,7 @@ suite("Variable iteration (copy blocks)", () => { "var2": "hello 2", } }), - "id"); + fakeId); assert.deepStrictEqual( dt.topLevelScope.variableDefinitions.map(v => v.nameValue.unquotedValue), @@ -298,7 +301,7 @@ suite("Variable iteration (copy blocks)", () => { }); test("No errors", async () => { - const errors = await dt.errorsPromise; + const errors = await dt.getErrors(undefined); assert.deepStrictEqual(errors, []); }); @@ -391,7 +394,7 @@ suite("Variable iteration (copy blocks)", () => { } } }), - "id"); + fakeId); assert.deepStrictEqual(Json.asObjectValue(dt2.topLevelScope.getVariableDefinition('object')!.value)!.propertyNames, ["array1"]); }); @@ -409,7 +412,7 @@ suite("Variable iteration (copy blocks)", () => { } } }), - "id"); + fakeId); // Right now we don't process as a copy block. Backend does and gives an error. // CONSIDER: Add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) @@ -431,7 +434,7 @@ suite("Variable iteration (copy blocks)", () => { } } }), - "id"); + fakeId); // Right now we just don't process as a copy block // CONSIDER: Instead add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) @@ -452,7 +455,7 @@ suite("Variable iteration (copy blocks)", () => { } } }), - "id"); + fakeId); // Right now we just don't process as a copy block // CONSIDER: Instead add a parse error (https://dev.azure.com/devdiv/DevDiv/_boards/board/t/ARM%20Template%20Authoring/Stories/?workitem=1010078) diff --git a/test/colorization/inputs/expr-vs-string.is-expression.jsonc b/test/colorization/inputs/expr-vs-string.is-expression.jsonc index 69b8f7351..0edbdad78 100644 --- a/test/colorization/inputs/expr-vs-string.is-expression.jsonc +++ b/test/colorization/inputs/expr-vs-string.is-expression.jsonc @@ -9,7 +9,7 @@ // Multi-line expressions - the colorization can't peek onto another line to determine whether the // string ends with "]" (and therefore know it's an expression and not a string), so assume it is an expression // if a multi-line string starts with "[" - "$TEST10": "[concat('This is a ', 1, '-line ', 'expression ', 4, 'you!')]", //asdf + "$TEST10": "[concat('This is a ', 1, '-line ', 'expression ', 4, 'you!')]", "$TEST11": "[concat('This is a ', 3, '-line ', 'expression ', 4, ' you!')]" diff --git a/test/formatDocument.test.ts b/test/formatDocument.test.ts index ef0d50ad4..c024c8cda 100644 --- a/test/formatDocument.test.ts +++ b/test/formatDocument.test.ts @@ -13,7 +13,7 @@ import * as fs from 'fs'; import { ISuiteCallbackContext, ITestCallbackContext } from "mocha"; import * as path from 'path'; import { commands, languages, Range, Selection, TextDocument, TextEditor, window, workspace } from "vscode"; -import { armDeploymentLanguageId } from "../extension.bundle"; +import { armTemplateLanguageId } from "../extension.bundle"; import { diagnosticsTimeout, testFolder } from "./support/diagnostics"; import { ensureLanguageServerAvailable } from "./support/ensureLanguageServerAvailable"; import { getTempFilePath } from "./support/getTempFilePath"; @@ -46,8 +46,8 @@ suite("Format document", function (this: ISuiteCallbackContext): void { fs.writeFileSync(filePath, jsonUnformatted); let doc = await workspace.openTextDocument(filePath); let editor: TextEditor = await window.showTextDocument(doc); - if (!sourceIsFile && doc.languageId !== armDeploymentLanguageId) { - await languages.setTextDocumentLanguage(doc, armDeploymentLanguageId); + if (!sourceIsFile && doc.languageId !== armTemplateLanguageId) { + await languages.setTextDocumentLanguage(doc, armTemplateLanguageId); } // Now that we've opened a document that should start up the server, wait until we know it's actually available before trying diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 195f110ee..05a584722 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -74,7 +74,7 @@ suite("InsertItem", async (): Promise => { let textEditor = await window.showTextDocument(document); let ui = new MockUserInput(showInputBox); let insertItem = new InsertItem(ui); - let deploymentTemplate = new DeploymentTemplate(document.getText(), document.uri.toString()); + 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(); diff --git a/test/functional/paramFileCompletions.functional.test.ts b/test/functional/paramFileCompletions.functional.test.ts new file mode 100644 index 000000000..97d9521cf --- /dev/null +++ b/test/functional/paramFileCompletions.functional.test.ts @@ -0,0 +1,411 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length no-unnecessary-class +// tslint:disable:no-non-null-assertion object-literal-key-quotes variable-name no-constant-condition +// tslint:disable:prefer-template no-http-string + +import * as assert from 'assert'; +import { commands, Selection } from 'vscode'; +import { ext } from '../../extension.bundle'; +import { delay } from '../support/delay'; +import { IDeploymentParametersFile, IDeploymentTemplate } from "../support/diagnostics"; +import { getCompletionItemResolutionPromise, getCompletionItemsPromise, getDocumentChangedPromise } from '../support/getEventPromise'; +import { getDocumentMarkers, removeEOLMarker } from "../support/parseTemplate"; +import { stringify } from '../support/stringify'; +import { TempDocument, TempEditor, TempFile } from '../support/TempFile'; +import { testWithLanguageServer } from '../support/testWithLanguageServer'; + +const newParamCompletionLabel = `""`; + +const defaultTemplate = { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "required1": { + "type": "string" + }, + "required2": { + "type": "int" + }, + "optional1": { + "type": "int", + "defaultValue": { + "abc": "def" + } + } + } +}; + +suite("Functional parameter file completions", () => { + + function createCompletionsFunctionalTest( + testName: string, + params: string | Partial, + template: string | Partial | undefined, + insertSuggestionPrefix: string, // Insert the suggestion starting with this string + expectedResult: string + ): void { + testWithLanguageServer(testName, async () => { + let editor: TempEditor | undefined; + let templateFile: TempFile | undefined; + + try { + const { markers: { bang }, unmarkedText } = getDocumentMarkers(params); + expectedResult = removeEOLMarker(expectedResult); + + // Create template/params files + if (template) { + templateFile = new TempFile(stringify(template)); + } + let paramsFile = new TempFile(unmarkedText); + + // Map template to params + if (templateFile) { + await ext.deploymentFileMapping.getValue().mapParameterFile(templateFile.uri, paramsFile.uri); + } + + // Open params in editor + const paramsDoc = new TempDocument(paramsFile); + editor = new TempEditor(paramsDoc); + await editor.open(); + + // Move cursor to the "!" in the document + const position = editor.document.realDocument.positionAt(bang.index); + editor.realEditor.selection = new Selection(position, position); + await delay(1); + + // Trigger completion UI + const completionItemsPromise = getCompletionItemsPromise(paramsDoc.realDocument); + await commands.executeCommand('editor.action.triggerSuggest'); + + // Wait for our code to return completion items + let items = await completionItemsPromise; + items = items; + + // Wait for any resolution to be sure the UI is ready + const resolutionPromise = getCompletionItemResolutionPromise(); + await delay(1); + let currentItem = await resolutionPromise; + + // Select the item we want and accept it + let tries = 0; + while (true) { + if (tries++ > 100) { + assert.fail(`Did not find a completion item starting with "${insertSuggestionPrefix}"`); + } + + if (currentItem.label.startsWith(insertSuggestionPrefix)) { + break; + } + + const resolutionPromise2 = getCompletionItemResolutionPromise(); + await commands.executeCommand('selectNextSuggestion'); + await delay(1); + currentItem = await resolutionPromise2; + } + + const documentChangedPromise = getDocumentChangedPromise(paramsDoc.realDocument); + await commands.executeCommand('acceptSelectedSuggestion'); + + // Wait for it to get inserted + await documentChangedPromise; + + // Some completions have additional text edits, and vscode doesn't + // seem to have made all the changes when it fires didDocumentChange, + // so give a slight delay to allow it to finish + await delay(1); + + const actualResult = paramsDoc.realDocument.getText(); + assert.equal(actualResult, expectedResult); + } finally { + if (editor) { + await editor.dispose(); + } + if (templateFile) { + await ext.deploymentFileMapping.getValue().mapParameterFile(templateFile.uri, undefined); + } + } + }); + } + + suite("Completions for new parameters", async () => { + createCompletionsFunctionalTest( + "No template file, new parameter in blank section", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + !{EOL} + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "parameter1": { + "value": "value" + } + } +}` + ); + + createCompletionsFunctionalTest( + "No template file, new parameter after an existing one, comma already exists", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + }, + !{EOL} + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + }, + "parameter1": { + "value": "value" + } + } +}` + ); + + createCompletionsFunctionalTest( + "No template file, new parameter after an existing one, automatically add comma after old param", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + } + !{EOL} + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + }, + "parameter1": { + "value": "value" + } + } +}` + ); + + createCompletionsFunctionalTest( + "No template file, new parameter after an existing one, automatically add comma after old param - has comments", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + } + // some comments + !{EOL} + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + }, + // some comments + "parameter1": { + "value": "value" + } + } +}` + ); + + createCompletionsFunctionalTest( + "No template file, new parameter before an existing one, automatically adds comma after new parameter", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + !{EOL} + "PARAmeter2": { + "value": "string" + } + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "parameter1": { + "value": "value" + }, + "PARAmeter2": { + "value": "string" + } + } +}` + ); + + createCompletionsFunctionalTest( + "No template file, inside existing double quotes (or double quote trigger), removes double quotes when inserting", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "!" + } +}`, + undefined, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "parameter1": { + "value": "value" + } + } +}` + ); + + createCompletionsFunctionalTest( + "Template file one required param, new parameter in blank section", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + !{EOL} + } +}`, + defaultTemplate, + newParamCompletionLabel, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "parameter1": { + "value": "value" + } + } +}` + ); + }); + + suite("Completions for parameters in template file", async () => { + createCompletionsFunctionalTest( + "From required parameter", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + !{EOL} + } +}`, + defaultTemplate, + `"required1"`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "required1": { + "value": "" // TODO: Fill in parameter value + } + } +}` + ); + + createCompletionsFunctionalTest( + "From optional parameter", + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "required1": { + "value": "abc" + }, + !{EOL} + "required2": { + "value": "abc" + } + } +}`, + defaultTemplate, + `"optional1"`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "required1": { + "value": "abc" + }, + "optional1": { + "value": { + "abc": "def" + } + }, + "required2": { + "value": "abc" + } + } +}` + ); + + }); + + createCompletionsFunctionalTest( + "From optional parameter, no existing comma", + `{ +"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", +"contentVersion": "1.0.0.0", +"parameters": { + "required1": { + "value": "abc" + } + !{EOL} + "required2": { + "value": "abc" + } +} +}`, + defaultTemplate, + `"optional1"`, + `{ +"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", +"contentVersion": "1.0.0.0", +"parameters": { + "required1": { + "value": "abc" + }, + "optional1": { + "value": { + "abc": "def" + } + }, + "required2": { + "value": "abc" + } +} +}` + ); +}); diff --git a/test/functional/paramFiles.addMissingParameters.test.ts b/test/functional/paramFiles.addMissingParameters.test.ts new file mode 100644 index 000000000..9460ddabb --- /dev/null +++ b/test/functional/paramFiles.addMissingParameters.test.ts @@ -0,0 +1,394 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length no-unnecessary-class +// tslint:disable:no-non-null-assertion object-literal-key-quotes variable-name no-constant-condition +// tslint:disable:prefer-template no-http-string + +import * as assert from 'assert'; +import { commands } from 'vscode'; +import { ext } from '../../extension.bundle'; +import { IDeploymentParametersFile, IDeploymentTemplate } from "../support/diagnostics"; +import { getDocumentMarkers, removeEOLMarker } from "../support/parseTemplate"; +import { stringify } from '../support/stringify'; +import { TempDocument, TempEditor, TempFile } from '../support/TempFile'; +import { testWithLanguageServer } from '../support/testWithLanguageServer'; + +const longTemplate = { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredString": { + "type": "string" + }, + "requiredArray": { + "type": "int" + }, + "optionalObject": { + "type": "int", + "defaultValue": { + "abc": "def" + } + }, + "optionalInt": { + "type": "int", + "defaultValue": 1 + } + } +}; + +const templateWithOneOptionalParam = { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "optionalInt": { + "type": "int", + "defaultValue": 1 + } + } +}; + +suite("Add missing parameters - functional", () => { + + enum Params { + "all", + "onlyRequired" + } + + function createAddMissingParamsTestAllAndRequired( + testName: string, + params: string | Partial, + template: string | Partial | undefined, + expectedResultForAllParameters: string, + expectedResultForOnlyRequiredParameters: string + ): void { + createAddMissingParamsTest( + `${testName} - all Parameters`, + params, + template, + Params.all, + expectedResultForAllParameters); + + createAddMissingParamsTest( + `${testName} - only required parameters`, + params, + template, + Params.onlyRequired, + expectedResultForOnlyRequiredParameters); + } + + function createAddMissingParamsTest( + testName: string, + params: string | Partial, + template: string | Partial | undefined, + whichParams: Params, + expectedResult: string + ): void { + testWithLanguageServer(testName, async () => { + let editor: TempEditor | undefined; + let templateFile: TempFile | undefined; + + try { + const { unmarkedText } = getDocumentMarkers(params); + expectedResult = removeEOLMarker(expectedResult); + + // Create template/params files + if (template) { + templateFile = new TempFile(stringify(template, 4)); + } + let paramsFile = new TempFile(unmarkedText); + + // Map template to params + if (templateFile) { + await ext.deploymentFileMapping.getValue().mapParameterFile(templateFile.uri, paramsFile.uri); + } + + // Open params in editor + const paramsDoc = new TempDocument(paramsFile); + editor = new TempEditor(paramsDoc); + await editor.open(); + + await commands.executeCommand( + whichParams === Params.all + ? 'azurerm-vscode-tools.codeAction.addAllMissingParameters' + : 'azurerm-vscode-tools.codeAction.addMissingRequiredParameters' + ); + + const actualResult = paramsDoc.realDocument.getText(); + assert.equal(actualResult, expectedResult); + } finally { + if (editor) { + await editor.dispose(); + } + if (templateFile) { + await ext.deploymentFileMapping.getValue().mapParameterFile(templateFile.uri, undefined); + } + } + }); + } + + createAddMissingParamsTestAllAndRequired( + "Empty parameters section", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + } +}`, + longTemplate, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredString": { + "value": "" // TODO: Fill in parameter value + }, + "requiredArray": { + "value": 0 // TODO: Fill in parameter value + }, + "optionalObject": { + "value": { + "abc": "def" + } + }, + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredString": { + "value": "" // TODO: Fill in parameter value + }, + "requiredArray": { + "value": 0 // TODO: Fill in parameter value + } + } +}` + ); + + createAddMissingParamsTestAllAndRequired( + "Empty parameters section with no whitespace", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +}`, + templateWithOneOptionalParam, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +}` + ); + + createAddMissingParamsTestAllAndRequired( + "Comma after existing param", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "unknownParameter": { + "value": 1 + } + } +}`, + templateWithOneOptionalParam, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "unknownParameter": { + "value": 1 + }, + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "unknownParameter": { + "value": 1 + } + } +}` + ); + + createAddMissingParamsTestAllAndRequired( + "Insert after existing param and comments", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "existingParam1": { + "value": 1 + } + // Hello + } +}`, + templateWithOneOptionalParam, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "existingParam1": { + "value": 1 + }, + // Hello + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "existingParam1": { + "value": 1 + } + // Hello + } +}` + ); + + createAddMissingParamsTestAllAndRequired( + "Insert after comments only", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Hello + } +}`, + templateWithOneOptionalParam, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Hello + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Hello + } +}` + ); + + createAddMissingParamsTestAllAndRequired( + "Some params aren't missing", + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredArray": { + "value": 0 // TODO: Fill in parameter value + }, + "optionalObject": { + "value": { + "abc": "def" + } + } + } +}`, + longTemplate, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredArray": { + "value": 0 // TODO: Fill in parameter value + }, + "optionalObject": { + "value": { + "abc": "def" + } + }, + "requiredString": { + "value": "" // TODO: Fill in parameter value + }, + "optionalInt": { + "value": 1 + } + } +}`, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "requiredArray": { + "value": 0 // TODO: Fill in parameter value + }, + "optionalObject": { + "value": { + "abc": "def" + } + }, + "requiredString": { + "value": "" // TODO: Fill in parameter value + } + } +}` + ); + + createAddMissingParamsTest( + "Don't put default value into params file if it's an expression", + stringify( + { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + } + }, + 4), + { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "defValueIsBoolLiteral": { + "type": "string", + defaultValue: true + }, + "defValueIsExpression": { + "type": "string", + defaultValue: "[variables('bool')]" + } + } + }, + Params.all, + `{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "defValueIsBoolLiteral": { + "value": true + }, + "defValueIsExpression": { + "value": "" // TODO: Fill in parameter value + } + } +}`); + +}); diff --git a/test/functional/snippets.test.ts b/test/functional/snippets.test.ts index 365bcfecb..a5dca2287 100644 --- a/test/functional/snippets.test.ts +++ b/test/functional/snippets.test.ts @@ -13,7 +13,7 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import { ITestCallbackContext } from 'mocha'; import * as path from 'path'; -import { commands, Diagnostic, Selection, window, workspace } from "vscode"; +import { commands, Diagnostic, Selection, Uri, window, workspace } from "vscode"; import { DeploymentTemplate, getVSCodePositionFromPosition } from '../../extension.bundle'; import { delay } from '../support/delay'; import { getDiagnosticsForDocument, sources, testFolder } from '../support/diagnostics'; @@ -384,7 +384,7 @@ suite("Snippets functional tests", () => { const snippetInsertComment: string = overrideInsertPosition[snippetName] || "// Insert here: resource"; const snippetInsertIndex: number = template.indexOf(snippetInsertComment); assert(snippetInsertIndex >= 0, `Couldn't find location to insert snippet (looking for "${snippetInsertComment}")`); - const snippetInsertPos = getVSCodePositionFromPosition(new DeploymentTemplate(template, "fake template").getContextFromDocumentCharacterIndex(snippetInsertIndex).documentPosition); + const snippetInsertPos = getVSCodePositionFromPosition(new DeploymentTemplate(template, Uri.file("fake template")).getContextFromDocumentCharacterIndex(snippetInsertIndex, undefined).documentPosition); const tempPath = getTempFilePath(`snippet ${snippetName}`, '.azrm'); diff --git a/test/functional/sortTemplate.test.ts b/test/functional/sortTemplate.test.ts index 0ef2d1d7f..8219dde93 100644 --- a/test/functional/sortTemplate.test.ts +++ b/test/functional/sortTemplate.test.ts @@ -12,6 +12,7 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import { commands, window, workspace } from "vscode"; import { getTempFilePath } from "../support/getTempFilePath"; +import { DISABLE_SLOW_TESTS } from '../testConstants'; suite("SortTemplate", async (): Promise => { const topLevelCommand = 'azurerm-vscode-tools.sortTopLevel'; @@ -21,6 +22,10 @@ suite("SortTemplate", async (): Promise => { const outputsCommand = 'azurerm-vscode-tools.sortOutputs'; const functionsCommand = 'azurerm-vscode-tools.sortFunctions'; + if (DISABLE_SLOW_TESTS) { + return; + } + async function testSortTemplate(command: string, template: String, expected: String): Promise { const tempPath = getTempFilePath(`sortTemplate`, '.azrm'); diff --git a/test/functional/validation.regression.test.ts b/test/functional/validation.regression.test.ts index c91d9f153..d9e810108 100644 --- a/test/functional/validation.regression.test.ts +++ b/test/functional/validation.regression.test.ts @@ -98,7 +98,7 @@ suite("Validation regression tests", () => { "Error: Expected a comma (','). (arm-template (expressions))", // Expected schema errors: - `Warning: Value must conform to exactly one of the associated schemas${os.EOL} Value must conform to exactly one of the associated schemas${os.EOL} Value must be one of the following types: boolean${os.EOL} or${os.EOL} Value must match the regular expression ^\\[([^\\[].*)?\\]$ at #/resources/3/properties/overprovision${os.EOL} or${os.EOL} Value must be one of the following types: string (arm-template (schema))` + `Warning: Value must conform to exactly one of the associated schemas${os.EOL}| Value must be one of the following types: boolean${os.EOL}| or${os.EOL}| Value must match the regular expression ^\\[([^\\[].*)?\\]$ (arm-template (schema))` ] ) ); diff --git a/test/functional/validation.test.ts b/test/functional/validation.test.ts new file mode 100644 index 000000000..58ae96368 --- /dev/null +++ b/test/functional/validation.test.ts @@ -0,0 +1,73 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-suspicious-comment + +import { testDiagnostics } from "../support/diagnostics"; +import { testWithLanguageServerAndRealFunctionMetadata } from "../support/testWithLanguageServer"; + +suite("General validation tests (all diagnotic sources)", () => { + suite("scoped deployments", () => { + testWithLanguageServerAndRealFunctionMetadata("invalid schema", async () => { + await testDiagnostics( + 'templates/scopes/invalid-schema.json', + { + }, + [ + "Error: Template validation failed: Template schema 'https://schema.management.azure.com/schemas/2015-01-02/deploymentTemplate.json#' is not supported. Supported versions are '2014-04-01-preview,2015-01-01,2018-05-01,2019-04-01,2019-08-01'. Please see https://aka.ms/arm-template for usage details. (arm-template (validation))", + "Warning: Unknown schema: https://schema.management.azure.com/schemas/2015-01-02/deploymentTemplate.json# (arm-template (schema))" + ]); + }); + + testWithLanguageServerAndRealFunctionMetadata("management group deployment", async () => { + await testDiagnostics( + 'templates/scopes/managementGroupDeploymentTemplate.define-policy.json', + { + }, + []); + }); + + testWithLanguageServerAndRealFunctionMetadata("resource group deployment - old root schema", async () => { + await testDiagnostics( + 'templates/scopes/resourceGroupDeployment2015-01-01.json', + { + }, + []); + }); + + testWithLanguageServerAndRealFunctionMetadata("resource group deployment - new root schema", async () => { + await testDiagnostics( + 'templates/scopes/resourceGroupDeployment2019-04-01.json', + { + }, + []); + }); + + testWithLanguageServerAndRealFunctionMetadata("subscription deployment", async () => { + await testDiagnostics( + 'templates/scopes/subscriptionDeploymentTemplate.json', + { + }, + [ + ]); + }); + + testWithLanguageServerAndRealFunctionMetadata("subscription deployment with nested resource group deployment", async () => { + await testDiagnostics( + 'templates/scopes/subscriptionDeploymentWithNesteRGDeployment.json', + { + }, + [ + ]); + }); + + testWithLanguageServerAndRealFunctionMetadata("tenant deployment", async () => { + await testDiagnostics( + 'templates/scopes/tenantDeploymentTemplate.assign-role.json', + { + }, + []); + }); + }); +}); diff --git a/test/global.test.ts b/test/global.test.ts index d2e7508c8..a602eea93 100644 --- a/test/global.test.ts +++ b/test/global.test.ts @@ -5,7 +5,7 @@ import * as mocha from 'mocha'; import * as vscode from 'vscode'; -import { configKeys, configPrefix, ext, languageId } from "../extension.bundle"; +import { armTemplateLanguageId, configKeys, configPrefix, ext } from "../extension.bundle"; import { displayCacheStatus, packageCache } from './support/clearCache'; import { delay } from "./support/delay"; import { useTestFunctionMetadata } from "./TestData"; @@ -41,7 +41,7 @@ suiteSetup(async function (this: mocha.IHookCallbackContext): Promise { vscode.workspace.getConfiguration(configPrefix).update(configKeys.autoDetectJsonTemplates, true, vscode.ConfigurationTarget.Global); // ... Add {'*.azrm':'arm-template'} to file.assocations (so colorization tests use the correct grammar, since _workbench.captureSyntaxTokens doesn't actually load anything into an editor) let fileAssociations = previousSettings.fileAssociations = vscode.workspace.getConfiguration('files').get<{}>('associations'); - let newAssociations = Object.assign({}, fileAssociations, { '*.azrm': languageId }); + let newAssociations = Object.assign({}, fileAssociations, { '*.azrm': armTemplateLanguageId }); vscode.workspace.getConfiguration('files', null).update('associations', newAssociations, vscode.ConfigurationTarget.Global); await delay(1000); // Give vscode time to update the setting diff --git a/test/parameterFileCompletions.test.ts b/test/parameterFileCompletions.test.ts new file mode 100644 index 000000000..861c92b0c --- /dev/null +++ b/test/parameterFileCompletions.test.ts @@ -0,0 +1,298 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length no-unnecessary-class +// tslint:disable:no-non-null-assertion object-literal-key-quotes variable-name no-constant-condition + +import * as assert from 'assert'; +import { isNullOrUndefined } from 'util'; +import { DeploymentTemplate } from "../extension.bundle"; +import { IDeploymentParametersFile, IDeploymentTemplate } from "./support/diagnostics"; +import { parseParametersWithMarkers, parseTemplate } from "./support/parseTemplate"; + +const newParamCompletionLabel = `""`; + +suite("Parameter file completions", () => { + + const emptyTemplate: string = `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + }`; + + function createParamsCompletionsTest( + testName: string, + params: string | Partial, + template: string | Partial | undefined, + options: { + cursorIndex?: number; + }, + // Can either be an array of completion names, or an array of + // [completion name, insert text] tuples + expectedNamesAndInsertTexts: ([string, string][]) | (string[]) + ): void { + const fullName = isNullOrUndefined(options.cursorIndex) ? testName : `${testName}, index=${options.cursorIndex}`; + test(fullName, async () => { + let dt: DeploymentTemplate | undefined = template ? await parseTemplate(template) : undefined; + + const { dp, markers: { bang } } = await parseParametersWithMarkers(params); + const cursorIndex = !isNullOrUndefined(options.cursorIndex) ? options.cursorIndex : bang.index; + if (isNullOrUndefined(cursorIndex)) { + assert.fail(`Expected either a cursor index in options or a "!" in the parameters file`); + } + + const pc = dp.getContextFromDocumentCharacterIndex(cursorIndex, dt); + const completions = pc.getCompletionItems(); + + const completionNames = completions.map(c => c.label).sort(); + const completionInserts = completions.map(c => c.insertText).sort(); + + const expectedNames = (expectedNamesAndInsertTexts).map(e => Array.isArray(e) ? e[0] : e).sort(); + // tslint:disable-next-line: no-any + const expectedInsertTexts = expectedNamesAndInsertTexts.every((e: any) => Array.isArray(e)) ? (<[string, string][]>expectedNamesAndInsertTexts).map(e => e[1]).sort() : undefined; + + assert.deepStrictEqual(completionNames, expectedNames, "Completion names didn't match"); + if (expectedInsertTexts !== undefined) { + assert.deepStrictEqual(completionInserts, expectedInsertTexts, "Completion insert texts didn't match"); + } + }); + } + + // ========================= + + suite("Completions for new parameters", async () => { + suite("Params file with missing parameters section - no completions anywhere", () => { + const dpWithNoParametersSection: string = `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0" + }`; + + for (let i = 0; i < dpWithNoParametersSection.length + 1; ++i) { + createParamsCompletionsTest( + "missing parameters section", + dpWithNoParametersSection, + undefined, + { cursorIndex: i }, + []); + } + }); + + // NOTE: The canAddPropertyHere test under ParametersPositionContext.test.ts is very + // thorough about testing where parameter insertions are allowed, don't need to be + // thorough about that here, just the results from the completion list. + createParamsCompletionsTest( + "No associated template file - no missing param completions", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ! + } + }`, + undefined, + {}, + [ + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "Template has no parameters - only new param completions available", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ! + } + }`, + emptyTemplate, + {}, + [ + newParamCompletionLabel + ]); + + suite("Offer completions for properties from template that aren't already defined in param file", () => { + createParamsCompletionsTest( + "2 in template, 0 in params", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ! + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "p10": { + "type": "int" + }, + "p2": { + "type": "string" + } + } + }, + {}, + [ + `"p2" (required)`, + `"p10" (required)`, + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "2 in template, 1 in params", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "p2": { + "value": "string" + }, + ! + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "p10": { + "type": "int" + }, + "p2": { + "type": "string" + } + } + }, + {}, + [ + // p2 already exists in param file + `"p10" (required)`, + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "2 in template, 1 in params, different casing", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "PARAmeter2": { + "value": "string" + }, + ! + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "Parameter10": { + "type": "int" + }, + "Parameter2": { + "type": "string" + } + } + }, + {}, + [ + // parameter2 already exists in param file + `"Parameter10" (required)`, // Use casing in template file + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "3 in template, 1 in params, different casing, cursor between two existing params", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "Parameter2": { + "value": "string" + }, + ! + "Parameter10": { + "value": "string" + } + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "Parameter10": { + "type": "int" + }, + "Parameter2": { + "type": "string" + }, + "Parameter30": { + "type": "string" + } + } + }, + {}, + [ + // parameter2 already exists in param file + `"Parameter30" (required)`, + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "2 in template, all of them in param file already", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "Parameter2": { + "value": "string" + }, + "Parameter10": { + "value": "string" + }, + ! + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "Parameter10": { + "type": "int" + }, + "Parameter2": { + "type": "string" + } + } + }, + {}, + [ + newParamCompletionLabel + ]); + + createParamsCompletionsTest( + "1 optional, 1 required", + `{ + $schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ! + } + }`, + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "parameters": { + "p1optional": { + "type": "string", + "defaultValue": "a" + }, + "p2required": { + "type": "string" + } + } + }, + {}, + [ + `"p1optional" (optional)`, + `"p2required" (required)`, + newParamCompletionLabel + ]); + }); + }); +}); diff --git a/test/support/TempFile.ts b/test/support/TempFile.ts new file mode 100644 index 000000000..ec861ed8b --- /dev/null +++ b/test/support/TempFile.ts @@ -0,0 +1,82 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// Support for testing diagnostics in vscode + +import * as assert from 'assert'; +import * as fs from 'fs'; +import { commands, TextDocument, TextEditor, Uri, window, workspace } from 'vscode'; +import { getTempFilePath } from './getTempFilePath'; + +export class TempFile { + public readonly fsPath: string; + public readonly uri: Uri; + + public constructor(contents: string, baseFilename?: string, extension?: string) { + this.fsPath = getTempFilePath(baseFilename, extension); + this.uri = Uri.file(this.fsPath); + + fs.writeFileSync(this.fsPath, contents); + } + + public dispose(): void { + if (fs.existsSync(this.fsPath)) { + fs.unlinkSync(this.fsPath); + } + } +} + +export class TempEditor { + private _editor: TextEditor | undefined; + + public constructor(public readonly document: TempDocument) { + } + + public get realEditor(): TextEditor { + assert(this._editor); + // tslint:disable-next-line:no-non-null-assertion + return this._editor!; + } + + public async open(): Promise { + if (!this._editor) { + await this.document.open(); + // tslint:disable-next-line:no-non-null-assertion + this._editor = await window.showTextDocument(this.document.realDocument!); + } + } + + public async dispose(): Promise { + await this.document.dispose(); + } +} + +export class TempDocument { + private _document: TextDocument | undefined; + public constructor(public readonly tempFile: TempFile) { + } + + public get realDocument(): TextDocument { + assert(this._document); + // tslint:disable-next-line:no-non-null-assertion + return this._document!; + } + public async open(): Promise { + if (!this._document) { + this._document = await workspace.openTextDocument(this.tempFile.fsPath); + } + } + + public async dispose(): Promise { + this.tempFile.dispose(); + + // NOTE: Even though we request the editor to be closed, + // there's no way to request the document actually be closed, + // and when you open it via an API, it doesn't close for a while, + // so the diagnostics won't go away + // See https://github.com/Microsoft/vscode/issues/43056 + await commands.executeCommand('workbench.action.closeActiveEditor'); + await commands.executeCommand('workbench.action.closeAllEditors'); + } +} diff --git a/test/support/TestConfiguration.ts b/test/support/TestConfiguration.ts new file mode 100644 index 000000000..eebb3aee0 --- /dev/null +++ b/test/support/TestConfiguration.ts @@ -0,0 +1,29 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import * as assert from 'assert'; +import { ConfigurationTarget } from "vscode"; +import { IConfiguration } from "../../extension.bundle"; + +export class TestConfiguration implements IConfiguration { + // tslint:disable-next-line:variable-name + public Test_globalValues: Map = new Map(); + + // tslint:disable-next-line:no-reserved-keywords + public get(key: string): T | undefined { + return this.Test_globalValues.get(key); + } + + public inspect(key: string): { globalValue?: T | undefined } | undefined { + const value = this.Test_globalValues.get(key); + return { + globalValue: value + }; + } + + public async update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Promise { + assert(configurationTarget === ConfigurationTarget.Global, "NYI"); + this.Test_globalValues.set(section, value); + } +} diff --git a/test/support/armTest.ts b/test/support/armTest.ts deleted file mode 100644 index 5fe43500f..000000000 --- a/test/support/armTest.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ---------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// ---------------------------------------------------------------------------- - -import { ITest, ITestCallbackContext } from "mocha"; -import { DISABLE_LANGUAGE_SERVER } from "../testConstants"; -import { runWithRealFunctionMetadata } from "../TestData"; -import { diagnosticsTimeout } from "./diagnostics"; - -export function armTest( - expectation: string, - options: { - requiresLanguageServer?: boolean; - useRealFunctionMetadata?: boolean; - }, - callback?: (this: ITestCallbackContext) => Promise -): ITest { - return test( - expectation, - async function (this: ITestCallbackContext): Promise { - if (options.requiresLanguageServer && DISABLE_LANGUAGE_SERVER) { - console.log("Skipping test because DISABLE_LANGUAGE_SERVER is set"); - this.skip(); - } else { - if (options.requiresLanguageServer) { - this.timeout(diagnosticsTimeout); - } - - if (callback) { - if (options.useRealFunctionMetadata) { - // tslint:disable-next-line: no-unsafe-any - return await runWithRealFunctionMetadata(callback.bind(this)); - } else { - // tslint:disable-next-line: no-unsafe-any - return await callback.call(this); - } - } else { - test(expectation); // Pending test (no callback) - } - } - }); -} diff --git a/test/support/createCompletionsTest.ts b/test/support/createCompletionsTest.ts index ac64a21bc..33f138f29 100644 --- a/test/support/createCompletionsTest.ts +++ b/test/support/createCompletionsTest.ts @@ -9,6 +9,7 @@ import { parseTemplateWithMarkers } from "./parseTemplate"; import { stringify } from './stringify'; export function createExpressionCompletionsTest( + // Contains the text of the expression. '!' indicates the cursor location replacementWithBang: string, // Can either be an array of completion names, or an array of // [completion name, insert text] tuples @@ -31,7 +32,7 @@ export function createExpressionCompletionsTest( export function createCompletionsTest( template: string | Partial, - find: string, + find: string, // String to find and replace in the template (e.g. '') replacementWithBang: string, // Can either be an array of completion names, or an array of // [completion name, insert text] tuples @@ -42,10 +43,10 @@ export function createCompletionsTest( const { dt, markers: { bang } } = await parseTemplateWithMarkers(template); assert(bang, "Didn't find ! marker in text"); - const pc = dt.getContextFromDocumentCharacterIndex(bang.index); + const pc = dt.getContextFromDocumentCharacterIndex(bang.index, undefined); const completions = pc.getCompletionItems(); - const completionNames = completions.map(c => c.name).sort(); + const completionNames = completions.map(c => c.label).sort(); const completionInserts = completions.map(c => c.insertText).sort(); const expectedNames = (expectedNamesAndInsertTexts).map(e => Array.isArray(e) ? e[0] : e).sort(); diff --git a/test/support/diagnostics.ts b/test/support/diagnostics.ts index 8daa971c0..770431840 100644 --- a/test/support/diagnostics.ts +++ b/test/support/diagnostics.ts @@ -16,11 +16,11 @@ const DEBUG_BREAK_AFTER_DIAGNOSTICS_COMPLETE = false; import * as assert from "assert"; import * as fs from 'fs'; import * as path from 'path'; -import { commands, Diagnostic, DiagnosticSeverity, Disposable, languages, TextDocument, window, workspace } from "vscode"; +import { Diagnostic, DiagnosticSeverity, Disposable, languages, TextDocument } from "vscode"; import { diagnosticsCompletePrefix, expressionsDiagnosticsSource, ExpressionType, ext, LanguageServerState, languageServerStateSource } from "../../extension.bundle"; import { DISABLE_LANGUAGE_SERVER } from "../testConstants"; -import { getTempFilePath } from "./getTempFilePath"; import { stringify } from "./stringify"; +import { TempDocument, TempEditor, TempFile } from "./TempFile"; export const diagnosticsTimeout = 2 * 60 * 1000; // CONSIDER: Use this long timeout only for first test, or for suite setup export const testFolder = path.join(__dirname, '..', '..', '..', 'test'); @@ -75,6 +75,18 @@ export interface IDeploymentNamespaceDefinition { }; } +export interface IDeploymentParameterValue { + value: unknown; +} + +export interface IDeploymentParametersFile { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" | "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#"; + contentVersion: string; + parameters?: { + [key: string]: IDeploymentParameterValue; + }; +} + export interface IDeploymentTemplate { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" | "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" | string; @@ -278,7 +290,8 @@ export async function getDiagnosticsForTemplate( options?: IGetDiagnosticsOptions ): Promise { let templateContents: string | undefined; - let fileToDelete: string | undefined; + let tempPathSuffix: string = ''; + // tslint:disable-next-line: strict-boolean-expressions options = options || {}; @@ -287,6 +300,7 @@ export async function getDiagnosticsForTemplate( // It's a filename let sourcePath = path.join(testFolder, templateContentsOrFileName); templateContents = fs.readFileSync(sourcePath).toString(); + tempPathSuffix = path.basename(templateContentsOrFileName, path.extname(templateContentsOrFileName)); } else { // It's a string templateContents = templateContentsOrFileName; @@ -307,28 +321,15 @@ export async function getDiagnosticsForTemplate( templateContents = newContents; } - // Write to temp file - let tempPath = getTempFilePath(); - fs.writeFileSync(tempPath, templateContents); - fileToDelete = tempPath; - - let doc = await workspace.openTextDocument(tempPath); - await window.showTextDocument(doc); + const tempFile = new TempFile(templateContents, tempPathSuffix); + const document = new TempDocument(tempFile); + const editor = new TempEditor(document); + await editor.open(); - let diagnostics: Diagnostic[] = await getDiagnosticsForDocument(doc, options); + let diagnostics: Diagnostic[] = await getDiagnosticsForDocument(document.realDocument, options); assert(diagnostics); - // NOTE: Even though we request the editor to be closed, - // there's no way to request the document actually be closed, - // and when you open it via an API, it doesn't close for a while, - // so the diagnostics won't go away - // See https://github.com/Microsoft/vscode/issues/43056 - await commands.executeCommand('workbench.action.closeAllEditors'); - - if (fileToDelete) { - fs.unlinkSync(fileToDelete); - } - + await editor.dispose(); return diagnostics; } diff --git a/test/support/getEventPromise.ts b/test/support/getEventPromise.ts new file mode 100644 index 000000000..a26f0894d --- /dev/null +++ b/test/support/getEventPromise.ts @@ -0,0 +1,82 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +// tslint:disable:no-unused-expression max-func-body-length promise-function-async max-line-length no-unnecessary-class +// tslint:disable:no-non-null-assertion object-literal-key-quotes variable-name no-constant-condition +// tslint:disable:prefer-template + +import { CompletionItem, TextDocument, workspace } from 'vscode'; +import { ext, ICompletionsSpyResult } from '../../extension.bundle'; +import { delay } from '../support/delay'; + +const defaultTimeout: number = 30 * 1000; + +export function getEventPromise( + eventName: string, + executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: unknown) => void) => void, + timeout: number = defaultTimeout): Promise { + // tslint:disable-next-line:promise-must-complete + return new Promise( + async (resolve: (value?: T | PromiseLike) => void, reject: (reason?: unknown) => void): Promise => { + let completed = false; + executor( + (value?: T | PromiseLike) => { + completed = true; + resolve(value); + }, + (reason?: unknown) => { + completed = true; + reject(reason); + } + ); + + await delay(timeout); + if (!completed) { + reject(new Error(`Timed out waiting for event "${eventName}"`)); + } + }); +} + +export function getDocumentChangedPromise(document: TextDocument, timeout: number = defaultTimeout): Promise { + return getEventPromise( + "onDidChangeTextDocument", + (resolve, reject) => { + const disposable = workspace.onDidChangeTextDocument(e => { + console.warn("changed"); + if (e.document === document) { + disposable.dispose(); + resolve(document.getText()); + } + }); + }, + timeout); +} + +export function getCompletionItemsPromise(document: TextDocument, timeout: number = defaultTimeout): Promise { + return getEventPromise( + "onCompletionItems", + (resolve, reject) => { + const disposable = ext.completionItemsSpy.onCompletionItems(e => { + if (e.document.documentId.fsPath === document.uri.fsPath) { + disposable.dispose(); + resolve(e); + } + }); + }, + timeout); +} + +export function getCompletionItemResolutionPromise(item?: CompletionItem, timeout: number = defaultTimeout): Promise { + return getEventPromise( + "onCompletionItemResolved", + (resolve, reject) => { + const disposable = ext.completionItemsSpy.onCompletionItemResolved(e => { + if (!item || e === item) { + disposable.dispose(); + resolve(e); + } + }); + }, + timeout); +} diff --git a/test/support/parseTemplate.ts b/test/support/parseTemplate.ts index a52e190cb..b3bf084f5 100644 --- a/test/support/parseTemplate.ts +++ b/test/support/parseTemplate.ts @@ -3,7 +3,9 @@ // ---------------------------------------------------------------------------- import * as assert from 'assert'; -import { DeploymentTemplate, Issue } from "../../extension.bundle"; +import { Uri } from 'vscode'; +import { DeploymentParameters, DeploymentTemplate, Issue } from "../../extension.bundle"; +import { IDeploymentParametersFile } from './diagnostics'; import { stringify } from "./stringify"; /** @@ -33,12 +35,12 @@ export async function parseTemplateWithMarkers( expectedDiagnosticMessages?: string[], options?: { ignoreWarnings: boolean } ): Promise<{ dt: DeploymentTemplate; markers: Markers }> { - const { text: templateWithoutMarkers, markers } = getDocumentMarkers(template); - const dt: DeploymentTemplate = new DeploymentTemplate(templateWithoutMarkers, "parseTemplate() template"); + const { unmarkedText, markers } = getDocumentMarkers(template); + const dt: DeploymentTemplate = new DeploymentTemplate(unmarkedText, Uri.file("https://parseTemplate template")); // Always run these even if not checking against expected, to verify nothing throws - const errors: Issue[] = await dt.errorsPromise; - const warnings: Issue[] = dt.warnings; + const errors: Issue[] = await dt.getErrors(undefined); + const warnings: Issue[] = dt.getWarnings(); const errorMessages = errors.map(e => `Error: ${e.message}`); const warningMessages = warnings.map(e => `Warning: ${e.message}`); @@ -53,17 +55,45 @@ export async function parseTemplateWithMarkers( return { dt, markers }; } +/** + * Pass in a parameter file with positions marked using the notation + * Returns the parsed document without the tags, plus a dictionary of the tags and their positions + */ +export async function parseParametersWithMarkers( + json: string | Partial +): Promise<{ dp: DeploymentParameters; unmarkedText: string; markers: Markers }> { + const { unmarkedText, markers } = getDocumentMarkers(json); + const dp: DeploymentParameters = new DeploymentParameters(unmarkedText, Uri.file("https://test parameter file")); + + // Always run these even if not checking against expected, to verify nothing throws + // tslint:disable-next-line:no-unused-expression + dp.parametersObjectValue; + // tslint:disable-next-line:no-unused-expression + dp.parameterValues; + + return { dp, unmarkedText, markers }; +} + +export function removeEOLMarker(s: string): string { + // Remove {EOL} markers (as convenience for some test results expressed as strings to + // express in a literal string where the end of line is, etc.) + return s.replace(/{EOL}/g, ''); +} + /** * Pass in a template with positions marked using the notation * Returns the document without the tags, plus a dictionary of the tags and their positions */ -export function getDocumentMarkers(template: object | string): { text: string; markers: Markers } { +export function getDocumentMarkers(doc: object | string): { unmarkedText: string; markers: Markers } { let markers: Markers = {}; - template = typeof template === "string" ? template : stringify(template); + doc = typeof doc === "string" ? doc : stringify(doc); + let modified = doc; + + modified = removeEOLMarker(modified); // tslint:disable-next-line:no-constant-condition while (true) { - let match: RegExpMatchArray | null = template.match(//); + let match: RegExpMatchArray | null = modified.match(//); if (!match) { break; } @@ -75,18 +105,28 @@ export function getDocumentMarkers(template: object | string): { text: string; m markers[marker.name] = marker; // Remove marker from the document - template = template.slice(0, marker.index) + template.slice(index + match[0].length); + modified = modified.slice(0, marker.index) + modified.slice(index + match[0].length); } // Also look for shortcut marker "!" with id "bang" used in some tests - let bangIndex = template.indexOf('!'); + let bangIndex = modified.indexOf('!'); if (bangIndex >= 0) { markers.bang = { name: 'bang', index: bangIndex }; - template = template.slice(0, bangIndex) + template.slice(bangIndex + 1); + modified = modified.slice(0, bangIndex) + modified.slice(bangIndex + 1); + } + + const malformed = + modified.match(/!/) + || modified.match(/!<([a-zA-Z][a-zA-Z0-9]*)!?>?/) + || modified.match(/?/) + || modified.match(//) + ; + if (malformed) { + throw new Error(`Malformed marker "${malformed[0]}" in text: ${doc}`); } return { - text: template, + unmarkedText: modified, markers }; } diff --git a/test/support/stringify.ts b/test/support/stringify.ts index 43ca17695..162bac0b5 100644 --- a/test/support/stringify.ts +++ b/test/support/stringify.ts @@ -8,6 +8,6 @@ /** * Stringifies the object with newlines and indenting (JSON.stringfy(x) by default gives the minimum string representation) */ -export function stringify(v: unknown): string { - return JSON.stringify(v, undefined, 2); +export function stringify(v: unknown, tabSize: number = 2): string { + return JSON.stringify(v, undefined, tabSize); } diff --git a/test/support/testGetReferences.ts b/test/support/testGetReferences.ts index afcb98457..d4fd54018 100644 --- a/test/support/testGetReferences.ts +++ b/test/support/testGetReferences.ts @@ -17,12 +17,12 @@ import { DeploymentTemplate, ReferenceList } from '../../extension.bundle'; * await testFindReferences(dt, apiVersionReference.index, [apiVersionReference.index, apiVersionDef.index]); */ export async function testGetReferences(dt: DeploymentTemplate, cursorIndex: number, expectedReferenceIndices: number[]): Promise { - const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex); + const pc = dt.getContextFromDocumentCharacterIndex(cursorIndex, undefined); // tslint:disable-next-line: no-non-null-assertion const references: ReferenceList = pc.getReferences()!; assert(references, "Expected non-empty list of references"); - const indices = references.spans.map(r => r.startIndex).sort(); + const indices = references.references.map(r => r.span.startIndex).sort(); expectedReferenceIndices = expectedReferenceIndices.sort(); assert.deepStrictEqual(indices, expectedReferenceIndices); diff --git a/test/support/testOnPlatform.ts b/test/support/testOnPlatform.ts new file mode 100644 index 000000000..f30811178 --- /dev/null +++ b/test/support/testOnPlatform.ts @@ -0,0 +1,52 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { ITest, ITestCallbackContext } from "mocha"; +import { isWin32 } from "../../extension.bundle"; +import { diagnosticsTimeout } from "./diagnostics"; +import { ITestPreparation, ITestPreparationResult, testWithPrep } from "./testWithPrep"; + +export class RequiresWin32 implements ITestPreparation { + public static readonly instance: RequiresWin32 = new RequiresWin32(); + + public pretest(this: ITestCallbackContext): ITestPreparationResult { + if (!isWin32) { + return { + skipTest: "this is not a Windows platform" + }; + } else { + this.timeout(diagnosticsTimeout); + return {}; + } + } +} + +export class RequiresMacLinux implements ITestPreparation { + public static readonly instance: RequiresMacLinux = new RequiresMacLinux(); + + public pretest(this: ITestCallbackContext): ITestPreparationResult { + if (isWin32) { + return { + skipTest: "this is not a Mac/Linux platform" + }; + } else { + this.timeout(diagnosticsTimeout); + return {}; + } + } +} + +export function testOnWin32(expectation: string, callback?: (this: ITestCallbackContext) => Promise): ITest { + return testWithPrep( + expectation, + [RequiresWin32.instance], + callback); +} + +export function testOnMacLinux(expectation: string, callback?: (this: ITestCallbackContext) => Promise): ITest { + return testWithPrep( + expectation, + [RequiresWin32.instance], + callback); +} diff --git a/test/support/testStringAtEachIndex.ts b/test/support/testStringAtEachIndex.ts new file mode 100644 index 000000000..d0878ebc8 --- /dev/null +++ b/test/support/testStringAtEachIndex.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { __debugMarkPositionInString } from '../../extension.bundle'; +import { getDocumentMarkers } from "./parseTemplate"; + +// See testCanAddPropertyHere for example usage +export async function testStringAtEachIndex( + textWithMarkers: string, + expectedValues: { [marker: string]: T }, + getTestResult: (text: string, index: number) => T +): Promise { + // Make the true/false markers unique so we can pass them to parseParametersWithMarkers + for (let marker of Object.keys(expectedValues)) { + for (let i = 1; ; ++i) { + // Add a unique integer to the first non-unique marker found + const newString = textWithMarkers.replace(``, ``); + if (newString === textWithMarkers) { + break; + } else { + textWithMarkers = newString; + } + } + } + + const { markers, unmarkedText } = getDocumentMarkers(textWithMarkers); + + // Perform actual test at each possible position + let expectedResultHere: T | undefined; + for (let i = 0; i < unmarkedText.length; ++i) { + // Determine expected result + const currentMarker: string | undefined = Object.getOwnPropertyNames(markers).find(key => markers[key].index === i); + if (currentMarker) { + const nonUniqueMarkerName = currentMarker.replace(/\$[0-9]+$/, ''); + if (!(nonUniqueMarkerName in expectedValues)) { + assert.fail(`Unexpected marker name "${nonUniqueMarkerName}"`); + } + + expectedResultHere = expectedValues[nonUniqueMarkerName]; + } else { + if (i === 0) { + assert.fail("Must have a marker at the very beginning of the string"); + } + } + + // Make test call + const actualResultHere: T = getTestResult(unmarkedText, i); + + // Validate + assert.equal( + actualResultHere, + expectedResultHere, + `At index ${i}: ${__debugMarkPositionInString(unmarkedText, i)}`); + } +} diff --git a/test/support/testWithLanguageServer.ts b/test/support/testWithLanguageServer.ts index 07c920e3a..0e3f807c2 100644 --- a/test/support/testWithLanguageServer.ts +++ b/test/support/testWithLanguageServer.ts @@ -3,23 +3,34 @@ // ---------------------------------------------------------------------------- import { ITest, ITestCallbackContext } from "mocha"; -import { armTest } from "./armTest"; +import { DISABLE_LANGUAGE_SERVER } from "../testConstants"; +import { UseRealFunctionMetadata } from "../TestData"; +import { diagnosticsTimeout } from "./diagnostics"; +import { ITestPreparation, ITestPreparationResult, testWithPrep } from "./testWithPrep"; + +export class RequiresLanguageServer implements ITestPreparation { + public static readonly instance: RequiresLanguageServer = new RequiresLanguageServer(); + + public pretest(this: ITestCallbackContext): ITestPreparationResult { + if (DISABLE_LANGUAGE_SERVER) { + return { + skipTest: "DISABLE_LANGUAGE_SERVER is set" + }; + } else { + this.timeout(diagnosticsTimeout); + return {}; + } + } +} export function testWithLanguageServer(expectation: string, callback?: (this: ITestCallbackContext) => Promise): ITest { - return armTest( - expectation, - { - requiresLanguageServer: true - }, - callback); + return testWithLanguageServerAndRealFunctionMetadata(expectation, callback); } export function testWithLanguageServerAndRealFunctionMetadata(expectation: string, callback?: (this: ITestCallbackContext) => Promise): ITest { - return armTest( + return testWithPrep( expectation, - { - requiresLanguageServer: true, - useRealFunctionMetadata: true - }, + [UseRealFunctionMetadata.instance, + RequiresLanguageServer.instance], callback); } diff --git a/test/support/testWithPrep.ts b/test/support/testWithPrep.ts new file mode 100644 index 000000000..2ec53876f --- /dev/null +++ b/test/support/testWithPrep.ts @@ -0,0 +1,57 @@ +// ---------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------------------- + +import { ITest, ITestCallbackContext } from "mocha"; + +export interface ITestPreparation { + // Perform pretest preparations, and return a Disposable which will revert those changes + pretest(this: ITestCallbackContext): ITestPreparationResult; +} + +export interface ITestPreparationResult { + postTest?(): void; + + // If non-empty, skips the test, displaying the string as a message + skipTest?: string; +} + +export function testWithPrep(expectation: string, preparations: ITestPreparation[], callback?: (this: ITestCallbackContext) => Promise): ITest { + return test( + expectation, + async function (this: ITestCallbackContext): Promise { + const postTests: (() => void)[] = []; + + try { + if (!callback) { + // This is a pending test - just pass it through as a pending test + test(expectation); + return; + } + + // Perform pre-test preparations + for (let prep of preparations) { + const prepResult = prep.pretest.call(this); + if (prepResult.skipTest) { + console.log(`Skipping test because: ${prepResult.skipTest}`); + this.skip(); + return; + } + + if (prepResult.postTest) { + postTests.push(prepResult.postTest); + } + } + + // Perform the test + return await callback.call(this); + } + finally { + // Perform post-test preparations + for (let post of postTests) { + post(); + } + } + } + ); +} diff --git a/test/supported.test.ts b/test/supported.test.ts index 287f43a44..66f30b766 100644 --- a/test/supported.test.ts +++ b/test/supported.test.ts @@ -6,8 +6,11 @@ import * as assert from 'assert'; import * as os from 'os'; +import { Uri } from 'vscode'; import { containsArmSchema, DeploymentTemplate, isArmSchema } from "../extension.bundle"; +const fakeId = Uri.file("https://fake-id"); + suite("supported", () => { suite("doesJsonContainArmSchema(string)", () => { suite("Just uri", () => { @@ -94,7 +97,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(dt.hasArmSchemaUri()); }); @@ -107,7 +110,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(dt.hasArmSchemaUri()); }); @@ -122,7 +125,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(dt.hasArmSchemaUri()); }); @@ -133,7 +136,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(dt.hasArmSchemaUri()); }); @@ -148,7 +151,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); @@ -158,7 +161,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); @@ -169,7 +172,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); @@ -179,7 +182,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); @@ -191,7 +194,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); @@ -203,7 +206,7 @@ suite("supported", () => { "contentVersion": "1.0.0.0", `; assert(containsArmSchema(template)); - let dt = new DeploymentTemplate(template, "id"); + let dt = new DeploymentTemplate(template, fakeId); assert(!dt.hasArmSchemaUri()); }); diff --git a/test/templates/portal/new-vmscaleset1.json b/test/templates/portal/new-vmscaleset1.json index 632f5db7b..de9e5327e 100644 --- a/test/templates/portal/new-vmscaleset1.json +++ b/test/templates/portal/new-vmscaleset1.json @@ -1,5 +1,5 @@ { - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "location": { diff --git a/test/templates/portal/new-vmscaleset1.parameters.json b/test/templates/portal/new-vmscaleset1.parameters.json new file mode 100644 index 000000000..24b90eda7 --- /dev/null +++ b/test/templates/portal/new-vmscaleset1.parameters.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "value": "" // TODO: Fill in parameter value + }, + "virtualMachineScaleSetName": { + "value": "" // TODO: Fill in parameter value + }, + "singlePlacementGroup": { + "value": "" // TODO: Fill in parameter value + }, + "instanceSize": { + "value": "" // TODO: Fill in parameter value + }, + "instanceCount": { + "value": "" // TODO: Fill in parameter value + }, + "priority": { + "value": "" // TODO: Fill in parameter value + }, + "osDiskType": { + "value": "" // TODO: Fill in parameter value + }, + "addressPrefixes": { + "value": [ + // TODO: Fill in parameter value + ] + }, + "subnets": { + "value": [ + // TODO: Fill in parameter value + ] + }, + "virtualNetworkName": { + "value": "" // TODO: Fill in parameter value + }, + "networkSecurityGroups": { + "value": [ + // TODO: Fill in parameter value + ] + }, + "networkInterfaceConfigurations": { + "value": [ + // TODO: Fill in parameter value + ] + }, + "diagnosticStorageAccount": { + "value": "" // TODO: Fill in parameter value + }, + "diagnosticsStorageAccountKind": { + "value": "" // TODO: Fill in parameter value + }, + "diagnosticsStorageAccountType": { + "value": "" // TODO: Fill in parameter value + }, + "upgradePolicy": { + "value": "" // TODO: Fill in parameter value + }, + "adminUsername": { + "value": "" // TODO: Fill in parameter value + }, + "adminPassword": { + "value": "" // TODO: Fill in parameter value + }, + "evictionPolicy": { + "value": "" // TODO: Fill in parameter value + }, + "zone": { + "value": [ + // TODO: Fill in parameter value + ] + }, + "platformFaultDomainCount": { + "value": "" // TODO: Fill in parameter value + } + } +} \ No newline at end of file diff --git a/test/templates/scopes/invalid-schema.json b/test/templates/scopes/invalid-schema.json new file mode 100644 index 000000000..31f2da194 --- /dev/null +++ b/test/templates/scopes/invalid-schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-02/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "allowAzureIPs": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "serverName": { + "type": "string" + } + }, + "resources": [ + { + "condition": "[parameters('allowAzureIPs')]", + "type": "Microsoft.Sql/servers/firewallRules", + "apiVersion": "2015-05-01-preview", + "name": "AllowAllWindowsAzureIps", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers/', parameters('serverName'))]" + ], + "properties": { + "endIpAddress": "0.0.0.0", + "startIpAddress": "0.0.0.0" + } + } + ] +} \ No newline at end of file diff --git a/test/templates/scopes/managementGroupDeploymentTemplate.define-policy.json b/test/templates/scopes/managementGroupDeploymentTemplate.define-policy.json new file mode 100644 index 000000000..c5a2af69d --- /dev/null +++ b/test/templates/scopes/managementGroupDeploymentTemplate.define-policy.json @@ -0,0 +1,27 @@ +{ + // from https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deploy-to-management-group + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Authorization/policyDefinitions", + "apiVersion": "2018-05-01", + "name": "locationpolicy", + "properties": { + "policyType": "Custom", + "parameters": {}, + "policyRule": { + "if": { + "field": "location", + "equals": "northeurope" + }, + "then": { + "effect": "deny" + } + } + } + } + ] +} \ No newline at end of file diff --git a/test/templates/scopes/resourceGroupDeployment2015-01-01.json b/test/templates/scopes/resourceGroupDeployment2015-01-01.json new file mode 100644 index 000000000..31d7d9037 --- /dev/null +++ b/test/templates/scopes/resourceGroupDeployment2015-01-01.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "allowAzureIPs": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "serverName": { + "type": "string" + } + }, + "resources": [ + { + "condition": "[parameters('allowAzureIPs')]", + "type": "Microsoft.Sql/servers/firewallRules", + "apiVersion": "2015-05-01-preview", + "name": "AllowAllWindowsAzureIps", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers/', parameters('serverName'))]" + ], + "properties": { + "endIpAddress": "0.0.0.0", + "startIpAddress": "0.0.0.0" + } + } + ] +} \ No newline at end of file diff --git a/test/templates/scopes/resourceGroupDeployment2019-04-01.json b/test/templates/scopes/resourceGroupDeployment2019-04-01.json new file mode 100644 index 000000000..1710470de --- /dev/null +++ b/test/templates/scopes/resourceGroupDeployment2019-04-01.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "allowAzureIPs": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "serverName": { + "type": "string" + } + }, + "resources": [ + { + "condition": "[parameters('allowAzureIPs')]", + "type": "Microsoft.Sql/servers/firewallRules", + "apiVersion": "2015-05-01-preview", + "name": "AllowAllWindowsAzureIps", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers/', parameters('serverName'))]" + ], + "properties": { + "endIpAddress": "0.0.0.0", + "startIpAddress": "0.0.0.0" + } + } + ] +} \ No newline at end of file diff --git a/test/templates/scopes/subscriptionDeployment2.json b/test/templates/scopes/subscriptionDeployment2.json new file mode 100644 index 000000000..758325372 --- /dev/null +++ b/test/templates/scopes/subscriptionDeployment2.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "rgName": { + "type": "string" + }, + "rgLocation": { + "type": "string" + }, + "storagePrefix": { + "type": "string", + "maxLength": 11 + } + }, + "variables": { + "storageName": "[concat(parameters('storagePrefix'), uniqueString(subscription().id, parameters('rgName')))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('rgLocation')]", + "name": "[parameters('rgName')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('rgName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('rgName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2017-10-01", + "name": "[variables('storageName')]", + "location": "[parameters('rgLocation')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + } + ], + "outputs": {} + } + } + } + ], + "outputs": {} +} \ No newline at end of file diff --git a/test/templates/scopes/subscriptionDeploymentTemplate.json b/test/templates/scopes/subscriptionDeploymentTemplate.json new file mode 100644 index 000000000..2a4d447ad --- /dev/null +++ b/test/templates/scopes/subscriptionDeploymentTemplate.json @@ -0,0 +1,27 @@ +{ + // from https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deploy-to-subscription + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "rgName": { + "type": "string" + }, + "rgLocation": { + "type": "string" + } + }, + "variables": { + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "name": "[parameters('rgName')]", + "location": "[parameters('rgLocation')]", + "properties": { + } + } + ], + "outputs": { + } +} \ No newline at end of file diff --git a/test/templates/scopes/subscriptionDeploymentWithNesteRGDeployment.json b/test/templates/scopes/subscriptionDeploymentWithNesteRGDeployment.json new file mode 100644 index 000000000..758325372 --- /dev/null +++ b/test/templates/scopes/subscriptionDeploymentWithNesteRGDeployment.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "rgName": { + "type": "string" + }, + "rgLocation": { + "type": "string" + }, + "storagePrefix": { + "type": "string", + "maxLength": 11 + } + }, + "variables": { + "storageName": "[concat(parameters('storagePrefix'), uniqueString(subscription().id, parameters('rgName')))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('rgLocation')]", + "name": "[parameters('rgName')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('rgName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('rgName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2017-10-01", + "name": "[variables('storageName')]", + "location": "[parameters('rgLocation')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + } + ], + "outputs": {} + } + } + } + ], + "outputs": {} +} \ No newline at end of file diff --git a/test/templates/scopes/tenantDeploymentTemplate.assign-role.json b/test/templates/scopes/tenantDeploymentTemplate.assign-role.json new file mode 100644 index 000000000..f6d71cdda --- /dev/null +++ b/test/templates/scopes/tenantDeploymentTemplate.assign-role.json @@ -0,0 +1,36 @@ +{ + // from https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deploy-to-tenant + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "principalId if the user that will be given contributor access to the resourceGroup" + } + }, + "roleDefinitionId": { + "type": "string", + "defaultValue": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "metadata": { + "description": "roleDefinition for the assignment - default is owner" + } + } + }, + "variables": { + // This creates an idempotent guid for the role assignment + "roleAssignmentName": "[guid('/', parameters('principalId'), parameters('roleDefinitionId'))]" + }, + "resources": [ + { + "name": "[variables('roleAssignmentName')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2017-09-01", + "properties": { + "roleDefinitionId": "[tenantResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]", + "principalId": "[parameters('principalId')]", + "scope": "/" + } + } + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fd118af8a..f5364f1f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "sourceMap": true, "noUnusedLocals": true, "strict": true, - "alwaysStrict": true + "alwaysStrict": true, + "experimentalDecorators": true }, "exclude": [ "node_modules" From 4f0adc0c28d0d3027ed4cb1db303ba95dc9298e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 20:42:15 +0200 Subject: [PATCH 44/61] Fixed ending of merged files --- package.json | 2 +- src/AzureRMTools.ts | 2428 +++++++++++++++++++++---------------------- 2 files changed, 1215 insertions(+), 1215 deletions(-) diff --git a/package.json b/package.json index d5611d9b7..eec9bef60 100644 --- a/package.json +++ b/package.json @@ -516,4 +516,4 @@ "vscode-jsonrpc": "^4.0.0", "vscode-languageclient": "^4.4.0" } -} \ No newline at end of file +} diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index e200cca20..068e3a094 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -1,1397 +1,1397 @@ - /*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - // tslint:disable:promise-function-async max-line-length // Grandfathered in - - import * as assert from "assert"; - import * as fse from 'fs-extra'; - import * as path from 'path'; - import * as vscode from "vscode"; - import { AzureUserInput, callWithTelemetryAndErrorHandling, callWithTelemetryAndErrorHandlingSync, createAzExtOutputChannel, createTelemetryReporter, IActionContext, registerCommand, registerUIExtensionVariables, TelemetryProperties } from "vscode-azureextensionui"; - import { uninstallDotnet } from "./acquisition/dotnetAcquisition"; - import * as Completion from "./Completion"; - import { armTemplateLanguageId, configKeys, configPrefix, expressionsDiagnosticsCompletionMessage, expressionsDiagnosticsSource, extensionName, globalStateKeys } from "./constants"; - import { DeploymentDocument } from "./DeploymentDocument"; - import { DeploymentTemplate } from "./DeploymentTemplate"; - import { ext } from "./extensionVariables"; - import { Histogram } from "./Histogram"; - import * as Hover from './Hover'; - import { DefinitionKind } from "./INamedDefinition"; - import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; - import { getInsertItemType, InsertItem } from "./insertItem"; - import * as Json from "./JSON"; - import * as language from "./Language"; - import { reloadSchemas } from "./languageclient/reloadSchemas"; - import { startArmLanguageServer, stopArmLanguageServer } from "./languageclient/startArmLanguageServer"; - import { DeploymentFileMapping } from "./parameterFiles/DeploymentFileMapping"; - import { DeploymentParameters } from "./parameterFiles/DeploymentParameters"; - import { considerQueryingForParameterFile, getFriendlyPathToFile, openParameterFile, openTemplateFile, selectParameterFile } from "./parameterFiles/parameterFiles"; - import { setParameterFileContext } from "./parameterFiles/setParameterFileContext"; - import { IReferenceSite, PositionContext } from "./PositionContext"; - import { ReferenceList } from "./ReferenceList"; - import { resetGlobalState } from "./resetGlobalState"; - import { getPreferredSchema } from "./schemas"; - import { getFunctionParamUsage } from "./signatureFormatting"; - import { getQuickPickItems, sortTemplate, SortType } from "./sortTemplate"; - import { Stopwatch } from "./Stopwatch"; - import { mightBeDeploymentParameters, mightBeDeploymentTemplate, templateDocumentSelector, templateOrParameterDocumentSelector } from "./supported"; - import { survey } from "./survey"; - import { TemplatePositionContext } from "./TemplatePositionContext"; - import * as TLE from "./TLE"; - import { JsonOutlineProvider } from "./Treeview"; - import { UnrecognizedBuiltinFunctionIssue } from "./UnrecognizedFunctionIssues"; - import { normalizePath } from "./util/normalizePath"; - import { Cancellation } from "./util/throwOnCancel"; - import { onCompletionActivated, toVsCodeCompletionItem } from "./util/toVsCodeCompletionItem"; - import { getVSCodeRangeFromSpan } from "./util/vscodePosition"; - - interface IErrorsAndWarnings { - errors: language.Issue[]; - warnings: language.Issue[]; - } - - const invalidRenameError = "Only parameters, variables, user namespaces and user functions can be renamed."; - - // This method is called when your extension is activated - // Your extension is activated the very first time the command is executed - export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }): Promise { - ext.context = context; - ext.reporter = createTelemetryReporter(context); - ext.outputChannel = createAzExtOutputChannel(extensionName, configPrefix); - ext.ui = new AzureUserInput(context.globalState); - - context.subscriptions.push(ext.completionItemsSpy); - - ext.deploymentFileMapping.setValue(new DeploymentFileMapping(ext.configuration)); - - registerUIExtensionVariables(ext); +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +// tslint:disable:promise-function-async max-line-length // Grandfathered in + +import * as assert from "assert"; +import * as fse from 'fs-extra'; +import * as path from 'path'; +import * as vscode from "vscode"; +import { AzureUserInput, callWithTelemetryAndErrorHandling, callWithTelemetryAndErrorHandlingSync, createAzExtOutputChannel, createTelemetryReporter, IActionContext, registerCommand, registerUIExtensionVariables, TelemetryProperties } from "vscode-azureextensionui"; +import { uninstallDotnet } from "./acquisition/dotnetAcquisition"; +import * as Completion from "./Completion"; +import { armTemplateLanguageId, configKeys, configPrefix, expressionsDiagnosticsCompletionMessage, expressionsDiagnosticsSource, extensionName, globalStateKeys } from "./constants"; +import { DeploymentDocument } from "./DeploymentDocument"; +import { DeploymentTemplate } from "./DeploymentTemplate"; +import { ext } from "./extensionVariables"; +import { Histogram } from "./Histogram"; +import * as Hover from './Hover'; +import { DefinitionKind } from "./INamedDefinition"; +import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; +import { getInsertItemType, InsertItem } from "./insertItem"; +import * as Json from "./JSON"; +import * as language from "./Language"; +import { reloadSchemas } from "./languageclient/reloadSchemas"; +import { startArmLanguageServer, stopArmLanguageServer } from "./languageclient/startArmLanguageServer"; +import { DeploymentFileMapping } from "./parameterFiles/DeploymentFileMapping"; +import { DeploymentParameters } from "./parameterFiles/DeploymentParameters"; +import { considerQueryingForParameterFile, getFriendlyPathToFile, openParameterFile, openTemplateFile, selectParameterFile } from "./parameterFiles/parameterFiles"; +import { setParameterFileContext } from "./parameterFiles/setParameterFileContext"; +import { IReferenceSite, PositionContext } from "./PositionContext"; +import { ReferenceList } from "./ReferenceList"; +import { resetGlobalState } from "./resetGlobalState"; +import { getPreferredSchema } from "./schemas"; +import { getFunctionParamUsage } from "./signatureFormatting"; +import { getQuickPickItems, sortTemplate, SortType } from "./sortTemplate"; +import { Stopwatch } from "./Stopwatch"; +import { mightBeDeploymentParameters, mightBeDeploymentTemplate, templateDocumentSelector, templateOrParameterDocumentSelector } from "./supported"; +import { survey } from "./survey"; +import { TemplatePositionContext } from "./TemplatePositionContext"; +import * as TLE from "./TLE"; +import { JsonOutlineProvider } from "./Treeview"; +import { UnrecognizedBuiltinFunctionIssue } from "./UnrecognizedFunctionIssues"; +import { normalizePath } from "./util/normalizePath"; +import { Cancellation } from "./util/throwOnCancel"; +import { onCompletionActivated, toVsCodeCompletionItem } from "./util/toVsCodeCompletionItem"; +import { getVSCodeRangeFromSpan } from "./util/vscodePosition"; + +interface IErrorsAndWarnings { + errors: language.Issue[]; + warnings: language.Issue[]; +} + +const invalidRenameError = "Only parameters, variables, user namespaces and user functions can be renamed."; + +// This method is called when your extension is activated +// Your extension is activated the very first time the command is executed +export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }): Promise { + ext.context = context; + ext.reporter = createTelemetryReporter(context); + ext.outputChannel = createAzExtOutputChannel(extensionName, configPrefix); + ext.ui = new AzureUserInput(context.globalState); + + context.subscriptions.push(ext.completionItemsSpy); + + ext.deploymentFileMapping.setValue(new DeploymentFileMapping(ext.configuration)); + + registerUIExtensionVariables(ext); + + await callWithTelemetryAndErrorHandling('activate', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.telemetry.measurements.mainFileLoad = (perfStats.loadEndTime - perfStats.loadStartTime) / 1000; + actionContext.telemetry.properties.autoDetectJsonTemplates = String(vscode.workspace.getConfiguration(configPrefix).get(configKeys.autoDetectJsonTemplates)); + + context.subscriptions.push(new AzureRMTools(context)); + }); +} + +// this method is called when your extension is deactivated +export function deactivateInternal(): void { + // Nothing to do +} + +export class AzureRMTools { + private readonly _diagnosticsCollection: vscode.DiagnosticCollection; + private readonly _deploymentDocuments: Map = new Map(); + private readonly _filesAskedToUpdateSchemaThisSession: Set = new Set(); + private readonly _paramsStatusBarItem: vscode.StatusBarItem; + private _areDeploymentTemplateEventsHookedUp: boolean = false; + private _diagnosticsVersion: number = 0; + private _mapping: DeploymentFileMapping = ext.deploymentFileMapping.getValue(); + + // More information can be found about this definition at https://code.visualstudio.com/docs/extensionAPI/vscode-api#DecorationRenderOptions + // Several of these properties are CSS properties. More information about those can be found at https://www.w3.org/wiki/CSS/Properties + private readonly _braceHighlightDecorationType: vscode.TextEditorDecorationType = vscode.window.createTextEditorDecorationType({ + borderWidth: "1px", + borderStyle: "solid", + light: { + borderColor: "rgba(0, 0, 0, 0.2)", + backgroundColor: "rgba(0, 0, 0, 0.05)" + }, + dark: { + borderColor: "rgba(128, 128, 128, 0.5)", + backgroundColor: "rgba(128, 128, 128, 0.1)" + } + }); - await callWithTelemetryAndErrorHandling('activate', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.telemetry.measurements.mainFileLoad = (perfStats.loadEndTime - perfStats.loadStartTime) / 1000; - actionContext.telemetry.properties.autoDetectJsonTemplates = String(vscode.workspace.getConfiguration(configPrefix).get(configKeys.autoDetectJsonTemplates)); + // tslint:disable-next-line:max-func-body-length + constructor(context: vscode.ExtensionContext) { + const jsonOutline: JsonOutlineProvider = new JsonOutlineProvider(context); + ext.jsonOutlineProvider = jsonOutline; + context.subscriptions.push(vscode.window.registerTreeDataProvider("azurerm-vscode-tools.template-outline", jsonOutline)); - context.subscriptions.push(new AzureRMTools(context)); + registerCommand("azurerm-vscode-tools.treeview.goto", (_actionContext: IActionContext, range: vscode.Range) => jsonOutline.goToDefinition(range)); + registerCommand("azurerm-vscode-tools.completion-activated", (actionContext: IActionContext, args: object) => { + onCompletionActivated(actionContext, args); }); - } - - // this method is called when your extension is deactivated - export function deactivateInternal(): void { - // Nothing to do - } - - export class AzureRMTools { - private readonly _diagnosticsCollection: vscode.DiagnosticCollection; - private readonly _deploymentDocuments: Map = new Map(); - private readonly _filesAskedToUpdateSchemaThisSession: Set = new Set(); - private readonly _paramsStatusBarItem: vscode.StatusBarItem; - private _areDeploymentTemplateEventsHookedUp: boolean = false; - private _diagnosticsVersion: number = 0; - private _mapping: DeploymentFileMapping = ext.deploymentFileMapping.getValue(); - - // More information can be found about this definition at https://code.visualstudio.com/docs/extensionAPI/vscode-api#DecorationRenderOptions - // Several of these properties are CSS properties. More information about those can be found at https://www.w3.org/wiki/CSS/Properties - private readonly _braceHighlightDecorationType: vscode.TextEditorDecorationType = vscode.window.createTextEditorDecorationType({ - borderWidth: "1px", - borderStyle: "solid", - light: { - borderColor: "rgba(0, 0, 0, 0.2)", - backgroundColor: "rgba(0, 0, 0, 0.05)" - }, - dark: { - borderColor: "rgba(128, 128, 128, 0.5)", - backgroundColor: "rgba(128, 128, 128, 0.1)" + registerCommand('azurerm-vscode-tools.uninstallDotnet', async () => { + await stopArmLanguageServer(); + await uninstallDotnet(); + }); + registerCommand("azurerm-vscode-tools.reloadSchemas", async () => { + await reloadSchemas(); + }); + registerCommand("azurerm-vscode-tools.sortTemplate", async (_context: 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 sortType = await ext.ui.showQuickPick(getQuickPickItems(), { placeHolder: 'What do you want to sort?' }); + await this.sortTemplate(sortType.value, uri, editor); } }); - - // tslint:disable-next-line:max-func-body-length - constructor(context: vscode.ExtensionContext) { - const jsonOutline: JsonOutlineProvider = new JsonOutlineProvider(context); - ext.jsonOutlineProvider = jsonOutline; - context.subscriptions.push(vscode.window.registerTreeDataProvider("azurerm-vscode-tools.template-outline", jsonOutline)); - - registerCommand("azurerm-vscode-tools.treeview.goto", (_actionContext: IActionContext, range: vscode.Range) => jsonOutline.goToDefinition(range)); - registerCommand("azurerm-vscode-tools.completion-activated", (actionContext: IActionContext, args: object) => { - onCompletionActivated(actionContext, args); - }); - registerCommand('azurerm-vscode-tools.uninstallDotnet', async () => { - await stopArmLanguageServer(); - await uninstallDotnet(); - }); - registerCommand("azurerm-vscode-tools.reloadSchemas", async () => { - await reloadSchemas(); - }); - registerCommand("azurerm-vscode-tools.sortTemplate", async (_context: 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 sortType = await ext.ui.showQuickPick(getQuickPickItems(), { placeHolder: 'What do you want to sort?' }); - await this.sortTemplate(sortType.value, uri, editor); - } - }); - registerCommand("azurerm-vscode-tools.sortFunctions", async () => { - await this.sortTemplate(SortType.Functions); - }); - registerCommand("azurerm-vscode-tools.sortOutputs", async () => { - await this.sortTemplate(SortType.Outputs); - }); - registerCommand("azurerm-vscode-tools.sortParameters", async () => { - await this.sortTemplate(SortType.Parameters); - }); - registerCommand("azurerm-vscode-tools.sortResources", async () => { - await this.sortTemplate(SortType.Resources); - }); - registerCommand("azurerm-vscode-tools.sortVariables", async () => { - await this.sortTemplate(SortType.Variables); - }); - registerCommand("azurerm-vscode-tools.sortTopLevel", async () => { - await this.sortTemplate(SortType.TopLevel); - }); - registerCommand( - "azurerm-vscode-tools.selectParameterFile", async (actionContext: IActionContext, source?: vscode.Uri) => { - await selectParameterFile(actionContext, this._mapping, source); - }); - registerCommand( - "azurerm-vscode-tools.openParameterFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { - source = source ?? vscode.window.activeTextEditor?.document.uri; - await openParameterFile(this._mapping, source, undefined); - }); - registerCommand( - "azurerm-vscode-tools.openTemplateFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { - source = source ?? vscode.window.activeTextEditor?.document.uri; - await openTemplateFile(this._mapping, source, undefined); - }); - registerCommand("azurerm-vscode-tools.insertItem", async (_context: 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 sortType = await ext.ui.showQuickPick(getInsertItemType(), { placeHolder: 'What do you want to insert?' }); - await this.insertItem(sortType.value, uri, editor); - } - }); - registerCommand("azurerm-vscode-tools.insertParameter", async () => { - await this.insertItem(SortType.Parameters); - }); - registerCommand("azurerm-vscode-tools.insertVariable", async () => { - await this.insertItem(SortType.Variables); - }); - registerCommand("azurerm-vscode-tools.insertOutput", async () => { - await this.insertItem(SortType.Outputs); - }); - registerCommand("azurerm-vscode-tools.insertFunction", async () => { - await this.insertItem(SortType.Functions); - }); - registerCommand("azurerm-vscode-tools.insertResource", async () => { - await this.insertItem(SortType.Resources); + registerCommand("azurerm-vscode-tools.sortFunctions", async () => { + await this.sortTemplate(SortType.Functions); + }); + registerCommand("azurerm-vscode-tools.sortOutputs", async () => { + await this.sortTemplate(SortType.Outputs); + }); + registerCommand("azurerm-vscode-tools.sortParameters", async () => { + await this.sortTemplate(SortType.Parameters); + }); + registerCommand("azurerm-vscode-tools.sortResources", async () => { + await this.sortTemplate(SortType.Resources); + }); + registerCommand("azurerm-vscode-tools.sortVariables", async () => { + await this.sortTemplate(SortType.Variables); + }); + registerCommand("azurerm-vscode-tools.sortTopLevel", async () => { + await this.sortTemplate(SortType.TopLevel); + }); + registerCommand( + "azurerm-vscode-tools.selectParameterFile", async (actionContext: IActionContext, source?: vscode.Uri) => { + await selectParameterFile(actionContext, this._mapping, source); }); - registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); - registerCommand("azurerm-vscode-tools.codeAction.addAllMissingParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { - await this.addMissingParameters(actionContext, source, false); + registerCommand( + "azurerm-vscode-tools.openParameterFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { + source = source ?? vscode.window.activeTextEditor?.document.uri; + await openParameterFile(this._mapping, source, undefined); }); - registerCommand("azurerm-vscode-tools.codeAction.addMissingRequiredParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { - await this.addMissingParameters(actionContext, source, true); + registerCommand( + "azurerm-vscode-tools.openTemplateFile", async (_actionContext: IActionContext, source?: vscode.Uri) => { + source = source ?? vscode.window.activeTextEditor?.document.uri; + await openTemplateFile(this._mapping, source, undefined); }); + registerCommand("azurerm-vscode-tools.insertItem", async (_context: 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 sortType = await ext.ui.showQuickPick(getInsertItemType(), { placeHolder: 'What do you want to insert?' }); + await this.insertItem(sortType.value, uri, editor); + } + }); + registerCommand("azurerm-vscode-tools.insertParameter", async () => { + await this.insertItem(SortType.Parameters); + }); + registerCommand("azurerm-vscode-tools.insertVariable", async () => { + await this.insertItem(SortType.Variables); + }); + registerCommand("azurerm-vscode-tools.insertOutput", async () => { + await this.insertItem(SortType.Outputs); + }); + registerCommand("azurerm-vscode-tools.insertFunction", async () => { + await this.insertItem(SortType.Functions); + }); + registerCommand("azurerm-vscode-tools.insertResource", async () => { + await this.insertItem(SortType.Resources); + }); + registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); + registerCommand("azurerm-vscode-tools.codeAction.addAllMissingParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { + await this.addMissingParameters(actionContext, source, false); + }); + registerCommand("azurerm-vscode-tools.codeAction.addMissingRequiredParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { + await this.addMissingParameters(actionContext, source, true); + }); - this._paramsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - ext.context.subscriptions.push(this._paramsStatusBarItem); + this._paramsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + ext.context.subscriptions.push(this._paramsStatusBarItem); - vscode.window.onDidChangeActiveTextEditor(this.onActiveTextEditorChanged, this, context.subscriptions); - vscode.workspace.onDidOpenTextDocument(this.onDocumentOpened, this, context.subscriptions); - vscode.workspace.onDidChangeTextDocument(this.onDocumentChanged, this, context.subscriptions); - vscode.workspace.onDidChangeConfiguration( - async () => { - this._mapping.resetCache(); - // tslint:disable-next-line: no-floating-promises - this.updateEditorState(); - }, - this, - context.subscriptions); - - this._diagnosticsCollection = vscode.languages.createDiagnosticCollection("azurerm-tools-expressions"); - context.subscriptions.push(this._diagnosticsCollection); - - const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (activeEditor) { - const activeDocument = activeEditor.document; - this.updateOpenedDocument(activeDocument); - } - } + vscode.window.onDidChangeActiveTextEditor(this.onActiveTextEditorChanged, this, context.subscriptions); + vscode.workspace.onDidOpenTextDocument(this.onDocumentOpened, this, context.subscriptions); + vscode.workspace.onDidChangeTextDocument(this.onDocumentChanged, this, context.subscriptions); + vscode.workspace.onDidChangeConfiguration( + async () => { + this._mapping.resetCache(); + // tslint:disable-next-line: no-floating-promises + this.updateEditorState(); + }, + this, + context.subscriptions); - private async addMissingParameters( - actionContext: IActionContext, - source: vscode.Uri | undefined, - onlyRequiredParameters: boolean - ): Promise { - source = source || vscode.window.activeTextEditor?.document.uri; - const editor = vscode.window.activeTextEditor; - const paramsUri = source || editor?.document.uri; - if (editor && paramsUri && editor.document.uri.fsPath === paramsUri.fsPath) { - let { doc, associatedDoc: template } = await this.getDeploymentDocAndAssociatedDoc(editor.document, Cancellation.cantCancel); - if (doc instanceof DeploymentParameters) { - await doc.addMissingParameters( - editor, - template, - onlyRequiredParameters); - } - } - } + this._diagnosticsCollection = vscode.languages.createDiagnosticCollection("azurerm-tools-expressions"); + context.subscriptions.push(this._diagnosticsCollection); - private async sortTemplate(sortType: SortType, 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); - } + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (activeEditor) { + const activeDocument = activeEditor.document; + this.updateOpenedDocument(activeDocument); } + } - private async insertItem(sortType: SortType, 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 new InsertItem(ext.ui).insertItem(deploymentTemplate, sortType, editor); + private async addMissingParameters( + actionContext: IActionContext, + source: vscode.Uri | undefined, + onlyRequiredParameters: boolean + ): Promise { + source = source || vscode.window.activeTextEditor?.document.uri; + const editor = vscode.window.activeTextEditor; + const paramsUri = source || editor?.document.uri; + if (editor && paramsUri && editor.document.uri.fsPath === paramsUri.fsPath) { + let { doc, associatedDoc: template } = await this.getDeploymentDocAndAssociatedDoc(editor.document, Cancellation.cantCancel); + if (doc instanceof DeploymentParameters) { + await doc.addMissingParameters( + editor, + template, + onlyRequiredParameters); } } + } - public dispose(): void { - callWithTelemetryAndErrorHandlingSync('dispose', (actionContext: IActionContext): void => { - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.errorHandling.suppressDisplay = true; - }); + private async sortTemplate(sortType: SortType, 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); } + } - // Add the deployment doc to our list of opened deployment docs - private setOpenedDeploymentDocument(documentUri: vscode.Uri, deploymentDocument: DeploymentDocument | undefined): void { - assert(documentUri); - const normalizedPath = normalizePath(documentUri); - if (deploymentDocument) { - this._deploymentDocuments.set(normalizedPath, deploymentDocument); - } else { - this._deploymentDocuments.delete(normalizedPath); - } + private async insertItem(sortType: SortType, 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 new InsertItem(ext.ui).insertItem(deploymentTemplate, sortType, editor); } + } - private getOpenedDeploymentDocument(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentDocument | undefined { - assert(documentOrUri); - const uri = documentOrUri instanceof vscode.Uri ? documentOrUri : documentOrUri.uri; - const normalizedPath = normalizePath(uri); - return this._deploymentDocuments.get(normalizedPath); - } + public dispose(): void { + callWithTelemetryAndErrorHandlingSync('dispose', (actionContext: IActionContext): void => { + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.errorHandling.suppressDisplay = true; + }); + } - private getOpenedDeploymentTemplate(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentTemplate | undefined { - const file = this.getOpenedDeploymentDocument(documentOrUri); - return file instanceof DeploymentTemplate ? file : undefined; + // Add the deployment doc to our list of opened deployment docs + private setOpenedDeploymentDocument(documentUri: vscode.Uri, deploymentDocument: DeploymentDocument | undefined): void { + assert(documentUri); + const normalizedPath = normalizePath(documentUri); + if (deploymentDocument) { + this._deploymentDocuments.set(normalizedPath, deploymentDocument); + } else { + this._deploymentDocuments.delete(normalizedPath); } + } - private getOpenedDeploymentParameters(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentParameters | undefined { - const file = this.getOpenedDeploymentDocument(documentOrUri); - return file instanceof DeploymentParameters ? file : undefined; - } + private getOpenedDeploymentDocument(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentDocument | undefined { + assert(documentOrUri); + const uri = documentOrUri instanceof vscode.Uri ? documentOrUri : documentOrUri.uri; + const normalizedPath = normalizePath(uri); + return this._deploymentDocuments.get(normalizedPath); + } - /** - * Analyzes a text document that has been opened, and handles it appropriately if - * it's a deployment template or parameter file - */ - private updateOpenedDocument(textDocument: vscode.TextDocument): void { - // tslint:disable-next-line:no-suspicious-comment - // TODO: refactor - // tslint:disable-next-line:max-func-body-length cyclomatic-complexity - callWithTelemetryAndErrorHandlingSync('updateDeploymentDocument', (actionContext: IActionContext): void => { - actionContext.errorHandling.suppressDisplay = true; - actionContext.telemetry.suppressIfSuccessful = true; - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.telemetry.properties.fileExt = path.extname(textDocument.fileName); + private getOpenedDeploymentTemplate(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentTemplate | undefined { + const file = this.getOpenedDeploymentDocument(documentOrUri); + return file instanceof DeploymentTemplate ? file : undefined; + } - assert(textDocument); - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - const stopwatch = new Stopwatch(); - stopwatch.start(); + private getOpenedDeploymentParameters(documentOrUri: vscode.TextDocument | vscode.Uri): DeploymentParameters | undefined { + const file = this.getOpenedDeploymentDocument(documentOrUri); + return file instanceof DeploymentParameters ? file : undefined; + } - let treatAsDeploymentTemplate = false; - let treatAsDeploymentParameters = false; - const documentUri = textDocument.uri; + /** + * Analyzes a text document that has been opened, and handles it appropriately if + * it's a deployment template or parameter file + */ + private updateOpenedDocument(textDocument: vscode.TextDocument): void { + // tslint:disable-next-line:no-suspicious-comment + // TODO: refactor + // tslint:disable-next-line:max-func-body-length cyclomatic-complexity + callWithTelemetryAndErrorHandlingSync('updateDeploymentDocument', (actionContext: IActionContext): void => { + actionContext.errorHandling.suppressDisplay = true; + actionContext.telemetry.suppressIfSuccessful = true; + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.telemetry.properties.fileExt = path.extname(textDocument.fileName); - if (textDocument.languageId === armTemplateLanguageId) { - // Lang ID is set to arm-template, whether auto or manual, respect the setting + assert(textDocument); + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + const stopwatch = new Stopwatch(); + stopwatch.start(); + + let treatAsDeploymentTemplate = false; + let treatAsDeploymentParameters = false; + const documentUri = textDocument.uri; + + if (textDocument.languageId === armTemplateLanguageId) { + // Lang ID is set to arm-template, whether auto or manual, respect the setting + treatAsDeploymentTemplate = true; + } + + // If the documentUri is not in our dictionary of deployment templates, then either + // it's not a deployment file, or else this document was just opened (as opposed + // to changed/updated). + // Note that it might have been opened, then closed, then reopened, or it + // might have had its schema changed in the editor to make it a deployment file. + const isNewlyOpened: boolean = !this.getOpenedDeploymentDocument(documentUri); + + // Is it a deployment template file? + let shouldParseFile = treatAsDeploymentTemplate || mightBeDeploymentTemplate(textDocument); + if (shouldParseFile) { + // Do a full parse + let deploymentTemplate: DeploymentTemplate = new DeploymentTemplate(textDocument.getText(), documentUri); + if (deploymentTemplate.hasArmSchemaUri()) { treatAsDeploymentTemplate = true; } + actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; - // If the documentUri is not in our dictionary of deployment templates, then either - // it's not a deployment file, or else this document was just opened (as opposed - // to changed/updated). - // Note that it might have been opened, then closed, then reopened, or it - // might have had its schema changed in the editor to make it a deployment file. - const isNewlyOpened: boolean = !this.getOpenedDeploymentDocument(documentUri); + if (treatAsDeploymentTemplate) { + this.ensureDeploymentDocumentEventsHookedUp(); + this.setOpenedDeploymentDocument(documentUri, deploymentTemplate); + survey.registerActiveUse(); - // Is it a deployment template file? - let shouldParseFile = treatAsDeploymentTemplate || mightBeDeploymentTemplate(textDocument); - if (shouldParseFile) { - // Do a full parse - let deploymentTemplate: DeploymentTemplate = new DeploymentTemplate(textDocument.getText(), documentUri); - if (deploymentTemplate.hasArmSchemaUri()) { - treatAsDeploymentTemplate = true; - } - actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; + if (isNewlyOpened) { + // A deployment template has been opened (as opposed to having been tabbed to) - if (treatAsDeploymentTemplate) { - this.ensureDeploymentDocumentEventsHookedUp(); - this.setOpenedDeploymentDocument(documentUri, deploymentTemplate); - survey.registerActiveUse(); + // Make sure the language ID is set to arm-template + if (textDocument.languageId !== armTemplateLanguageId) { + // The document will be reloaded, firing this event again with the new langid + AzureRMTools.setLanguageToArm(textDocument, actionContext); + return; + } + } + // Not waiting for return + // tslint:disable-next-line: no-floating-promises + this.reportDeploymentTemplateErrors(textDocument, deploymentTemplate).then(async (errorsWarnings) => { if (isNewlyOpened) { - // A deployment template has been opened (as opposed to having been tabbed to) + // Telemetry for template opened + if (errorsWarnings) { + this.reportTemplateOpenedTelemetry(textDocument, deploymentTemplate, stopwatch, errorsWarnings); + } + + // No guarantee that active editor is the one we're processing, ignore if not + if (editor && editor.document === textDocument) { + // Are they using an older schema? Ask to update. + // tslint:disable-next-line: no-suspicious-comment + // TODO: Move to separate file + this.considerQueryingForNewerSchema(editor, deploymentTemplate); - // Make sure the language ID is set to arm-template - if (textDocument.languageId !== armTemplateLanguageId) { - // The document will be reloaded, firing this event again with the new langid - AzureRMTools.setLanguageToArm(textDocument, actionContext); - return; + // Is there a possibly-matching params file they might want to associate? + considerQueryingForParameterFile(this._mapping, textDocument); } } + }); + } + } + + if (!treatAsDeploymentTemplate) { + // Is it a parameter file? + let shouldParseParameterFile = treatAsDeploymentTemplate || mightBeDeploymentParameters(textDocument); + if (shouldParseParameterFile) { + // Do a full parse + let deploymentParameters: DeploymentParameters = new DeploymentParameters(textDocument.getText(), textDocument.uri); + if (deploymentParameters.hasParametersUri()) { + treatAsDeploymentParameters = true; + } + + // This could theoretically include time for parsing for a deployment template as well but isn't likely + actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; + + if (treatAsDeploymentParameters) { + this.ensureDeploymentDocumentEventsHookedUp(); + this.setOpenedDeploymentDocument(documentUri, deploymentParameters); + survey.registerActiveUse(); - // Not waiting for return // tslint:disable-next-line: no-floating-promises - this.reportDeploymentTemplateErrors(textDocument, deploymentTemplate).then(async (errorsWarnings) => { - if (isNewlyOpened) { - // Telemetry for template opened - if (errorsWarnings) { - this.reportTemplateOpenedTelemetry(textDocument, deploymentTemplate, stopwatch, errorsWarnings); - } - - // No guarantee that active editor is the one we're processing, ignore if not - if (editor && editor.document === textDocument) { - // Are they using an older schema? Ask to update. - // tslint:disable-next-line: no-suspicious-comment - // TODO: Move to separate file - this.considerQueryingForNewerSchema(editor, deploymentTemplate); - - // Is there a possibly-matching params file they might want to associate? - considerQueryingForParameterFile(this._mapping, textDocument); - } + this.reportDeploymentParametersErrors(textDocument, deploymentParameters).then(async (errorsWarnings) => { + if (isNewlyOpened && errorsWarnings) { + // A deployment template has been opened (as opposed to having been tabbed to) + + // Telemetry for parameter file opened + await this.reportParameterFileOpenedTelemetry(textDocument, deploymentParameters, stopwatch, errorsWarnings); } }); } } - if (!treatAsDeploymentTemplate) { - // Is it a parameter file? - let shouldParseParameterFile = treatAsDeploymentTemplate || mightBeDeploymentParameters(textDocument); - if (shouldParseParameterFile) { - // Do a full parse - let deploymentParameters: DeploymentParameters = new DeploymentParameters(textDocument.getText(), textDocument.uri); - if (deploymentParameters.hasParametersUri()) { - treatAsDeploymentParameters = true; - } - - // This could theoretically include time for parsing for a deployment template as well but isn't likely - actionContext.telemetry.measurements.parseDurationInMilliseconds = stopwatch.duration.totalMilliseconds; + if (!treatAsDeploymentTemplate && !treatAsDeploymentParameters) { + // If the document is not a deployment file, then we need + // to remove it from our deployment file cache. It doesn't + // matter if the document is a JSON file and was never a + // deployment file, or if the document was a deployment + // file and then was modified to no longer be a deployment + // file (the $schema property changed to not be a + // template/params schema). In either case, we should + // remove it from our cache. + this.closeDeploymentFile(textDocument); + } - if (treatAsDeploymentParameters) { - this.ensureDeploymentDocumentEventsHookedUp(); - this.setOpenedDeploymentDocument(documentUri, deploymentParameters); - survey.registerActiveUse(); + // tslint:disable-next-line: no-floating-promises + this.updateEditorState(); + } + }); + } - // tslint:disable-next-line: no-floating-promises - this.reportDeploymentParametersErrors(textDocument, deploymentParameters).then(async (errorsWarnings) => { - if (isNewlyOpened && errorsWarnings) { - // A deployment template has been opened (as opposed to having been tabbed to) + private static setLanguageToArm(document: vscode.TextDocument, actionContext: IActionContext): void { + vscode.languages.setTextDocumentLanguage(document, armTemplateLanguageId); - // Telemetry for parameter file opened - await this.reportParameterFileOpenedTelemetry(textDocument, deploymentParameters, stopwatch, errorsWarnings); - } - }); - } - } + actionContext.telemetry.properties.switchedToArm = 'true'; + actionContext.telemetry.properties.docLangId = document.languageId; + actionContext.telemetry.properties.docExtension = path.extname(document.fileName); + actionContext.telemetry.suppressIfSuccessful = false; + } - if (!treatAsDeploymentTemplate && !treatAsDeploymentParameters) { - // If the document is not a deployment file, then we need - // to remove it from our deployment file cache. It doesn't - // matter if the document is a JSON file and was never a - // deployment file, or if the document was a deployment - // file and then was modified to no longer be a deployment - // file (the $schema property changed to not be a - // template/params schema). In either case, we should - // remove it from our cache. - this.closeDeploymentFile(textDocument); - } + private reportTemplateOpenedTelemetry( + document: vscode.TextDocument, + deploymentTemplate: DeploymentTemplate, + stopwatch: Stopwatch, + errorsWarnings: IErrorsAndWarnings + ): void { + // tslint:disable-next-line: restrict-plus-operands + const functionsInEachNamespace = deploymentTemplate.topLevelScope.namespaceDefinitions.map(ns => ns.members.length); + // tslint:disable-next-line: restrict-plus-operands + const totalUserFunctionsCount = functionsInEachNamespace.reduce((sum, count) => sum + count, 0); + + const issuesHistograph = new Histogram(); + for (const error of errorsWarnings.errors) { + issuesHistograph.add(`extErr:${error.kind}`); + } + for (const warning of errorsWarnings.warnings) { + issuesHistograph.add(`extWarn:${warning.kind}`); + } - // tslint:disable-next-line: no-floating-promises - this.updateEditorState(); - } + ext.reporter.sendTelemetryEvent( + "Deployment Template Opened", + { + docLangId: document.languageId, + docExtension: path.extname(document.fileName), + // tslint:disable-next-line: strict-boolean-expressions + schema: deploymentTemplate.schemaUri || "", + // tslint:disable-next-line: strict-boolean-expressions + apiProfile: deploymentTemplate.apiProfile || "", + issues: this.histogramToTelemetryString(issuesHistograph) + }, + { + documentSizeInCharacters: document.getText().length, + parseDurationInMilliseconds: stopwatch.duration.totalMilliseconds, + lineCount: deploymentTemplate.lineCount, + maxLineLength: deploymentTemplate.getMaxLineLength(), + paramsCount: deploymentTemplate.topLevelScope.parameterDefinitions.length, + varsCount: deploymentTemplate.topLevelScope.variableDefinitions.length, + namespacesCount: deploymentTemplate.topLevelScope.namespaceDefinitions.length, + userFunctionsCount: totalUserFunctionsCount, + multilineStringCount: deploymentTemplate.getMultilineStringCount(), + commentCount: deploymentTemplate.getCommentCount(), + extErrorsCount: errorsWarnings.errors.length, + extWarnCount: errorsWarnings.warnings.length, + linkedParameterFiles: this._mapping.getParameterFile(document.uri) ? 1 : 0 }); - } - private static setLanguageToArm(document: vscode.TextDocument, actionContext: IActionContext): void { - vscode.languages.setTextDocumentLanguage(document, armTemplateLanguageId); + this.logFunctionCounts(deploymentTemplate); + this.logResourceUsage(deploymentTemplate); + } - actionContext.telemetry.properties.switchedToArm = 'true'; - actionContext.telemetry.properties.docLangId = document.languageId; - actionContext.telemetry.properties.docExtension = path.extname(document.fileName); - actionContext.telemetry.suppressIfSuccessful = false; + private async reportParameterFileOpenedTelemetry( + document: vscode.TextDocument, + parameters: DeploymentParameters, + stopwatch: Stopwatch, + errorsWarnings: IErrorsAndWarnings + ): Promise { + const issuesHistograph = new Histogram(); + for (const error of errorsWarnings.errors) { + issuesHistograph.add(`extErr:${error.kind}`); + } + for (const warning of errorsWarnings.warnings) { + issuesHistograph.add(`extWarn:${warning.kind}`); } - private reportTemplateOpenedTelemetry( - document: vscode.TextDocument, - deploymentTemplate: DeploymentTemplate, - stopwatch: Stopwatch, - errorsWarnings: IErrorsAndWarnings - ): void { - // tslint:disable-next-line: restrict-plus-operands - const functionsInEachNamespace = deploymentTemplate.topLevelScope.namespaceDefinitions.map(ns => ns.members.length); - // tslint:disable-next-line: restrict-plus-operands - const totalUserFunctionsCount = functionsInEachNamespace.reduce((sum, count) => sum + count, 0); - - const issuesHistograph = new Histogram(); - for (const error of errorsWarnings.errors) { - issuesHistograph.add(`extErr:${error.kind}`); - } - for (const warning of errorsWarnings.warnings) { - issuesHistograph.add(`extWarn:${warning.kind}`); - } + ext.reporter.sendTelemetryEvent( + "Parameter File Opened", + { + docLangId: document.languageId, + docExtension: path.extname(document.fileName), + schema: parameters.schemaUri ?? "" + }, + { + documentSizeInCharacters: document.getText().length, + parseDurationInMilliseconds: stopwatch.duration.totalMilliseconds, + lineCount: parameters.lineCount, + maxLineLength: parameters.getMaxLineLength(), + paramsCount: parameters.parametersObjectValue?.length ?? 0, + commentCount: parameters.getCommentCount(), + linkedTemplateFiles: this._mapping.getTemplateFile(document.uri) ? 1 : 0, + extErrorsCount: errorsWarnings.errors.length, + extWarnCount: errorsWarnings.warnings.length + }); + } - ext.reporter.sendTelemetryEvent( - "Deployment Template Opened", - { - docLangId: document.languageId, - docExtension: path.extname(document.fileName), - // tslint:disable-next-line: strict-boolean-expressions - schema: deploymentTemplate.schemaUri || "", - // tslint:disable-next-line: strict-boolean-expressions - apiProfile: deploymentTemplate.apiProfile || "", - issues: this.histogramToTelemetryString(issuesHistograph) - }, - { - documentSizeInCharacters: document.getText().length, - parseDurationInMilliseconds: stopwatch.duration.totalMilliseconds, - lineCount: deploymentTemplate.lineCount, - maxLineLength: deploymentTemplate.getMaxLineLength(), - paramsCount: deploymentTemplate.topLevelScope.parameterDefinitions.length, - varsCount: deploymentTemplate.topLevelScope.variableDefinitions.length, - namespacesCount: deploymentTemplate.topLevelScope.namespaceDefinitions.length, - userFunctionsCount: totalUserFunctionsCount, - multilineStringCount: deploymentTemplate.getMultilineStringCount(), - commentCount: deploymentTemplate.getCommentCount(), - extErrorsCount: errorsWarnings.errors.length, - extWarnCount: errorsWarnings.warnings.length, - linkedParameterFiles: this._mapping.getParameterFile(document.uri) ? 1 : 0 - }); - - this.logFunctionCounts(deploymentTemplate); - this.logResourceUsage(deploymentTemplate); + private async reportDeploymentDocumentErrors( + textDocument: vscode.TextDocument, + deploymentDocument: DeploymentDocument, + associatedDocument: DeploymentDocument | undefined + ): Promise { + // Don't wait + // tslint:disable-next-line: no-floating-promises + ++this._diagnosticsVersion; + + let errors: language.Issue[] = await deploymentDocument.getErrors(associatedDocument); + const diagnostics: vscode.Diagnostic[] = []; + + for (const error of errors) { + diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, error, vscode.DiagnosticSeverity.Error)); } - private async reportParameterFileOpenedTelemetry( - document: vscode.TextDocument, - parameters: DeploymentParameters, - stopwatch: Stopwatch, - errorsWarnings: IErrorsAndWarnings - ): Promise { - const issuesHistograph = new Histogram(); - for (const error of errorsWarnings.errors) { - issuesHistograph.add(`extErr:${error.kind}`); - } - for (const warning of errorsWarnings.warnings) { - issuesHistograph.add(`extWarn:${warning.kind}`); - } + const warnings = deploymentDocument.getWarnings(); + for (const warning of warnings) { + diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, warning, vscode.DiagnosticSeverity.Warning)); + } - ext.reporter.sendTelemetryEvent( - "Parameter File Opened", - { - docLangId: document.languageId, - docExtension: path.extname(document.fileName), - schema: parameters.schemaUri ?? "" - }, - { - documentSizeInCharacters: document.getText().length, - parseDurationInMilliseconds: stopwatch.duration.totalMilliseconds, - lineCount: parameters.lineCount, - maxLineLength: parameters.getMaxLineLength(), - paramsCount: parameters.parametersObjectValue?.length ?? 0, - commentCount: parameters.getCommentCount(), - linkedTemplateFiles: this._mapping.getTemplateFile(document.uri) ? 1 : 0, - extErrorsCount: errorsWarnings.errors.length, - extWarnCount: errorsWarnings.warnings.length - }); + let completionDiagnostic = this.getCompletedDiagnostic(); + if (completionDiagnostic) { + diagnostics.push(completionDiagnostic); } - private async reportDeploymentDocumentErrors( - textDocument: vscode.TextDocument, - deploymentDocument: DeploymentDocument, - associatedDocument: DeploymentDocument | undefined - ): Promise { - // Don't wait - // tslint:disable-next-line: no-floating-promises - ++this._diagnosticsVersion; + this._diagnosticsCollection.set(textDocument.uri, diagnostics); - let errors: language.Issue[] = await deploymentDocument.getErrors(associatedDocument); - const diagnostics: vscode.Diagnostic[] = []; + return { errors, warnings }; + } - for (const error of errors) { - diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, error, vscode.DiagnosticSeverity.Error)); - } + private async reportDeploymentTemplateErrors( + textDocument: vscode.TextDocument, + deploymentTemplate: DeploymentTemplate + ): Promise { + return await callWithTelemetryAndErrorHandling('reportDeploymentTemplateErrors', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; - const warnings = deploymentDocument.getWarnings(); - for (const warning of warnings) { - diagnostics.push(this.getVSCodeDiagnosticFromIssue(deploymentDocument, warning, vscode.DiagnosticSeverity.Warning)); - } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Associated parameters + const associatedParameters: DeploymentParameters | undefined = undefined; + return await this.reportDeploymentDocumentErrors(textDocument, deploymentTemplate, associatedParameters); + }); + } - let completionDiagnostic = this.getCompletedDiagnostic(); - if (completionDiagnostic) { - diagnostics.push(completionDiagnostic); - } + private async reportDeploymentParametersErrors( + textDocument: vscode.TextDocument, + deploymentParameters: DeploymentParameters + ): Promise { + return await callWithTelemetryAndErrorHandling('reportDeploymentParametersErrors', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; - this._diagnosticsCollection.set(textDocument.uri, diagnostics); + const template = await this.getOrReadAssociatedTemplate(textDocument.uri, Cancellation.cantCancel); + return await this.reportDeploymentDocumentErrors(textDocument, deploymentParameters, template); + }); + } - return { errors, warnings }; + private considerQueryingForNewerSchema(editor: vscode.TextEditor, deploymentTemplate: DeploymentTemplate): void { + // Only deal with saved files, because we don't have an accurate + // URI that we can track for unsaved files, and it's a better user experience. + if (editor.document.uri.scheme !== 'file') { + return; } - private async reportDeploymentTemplateErrors( - textDocument: vscode.TextDocument, - deploymentTemplate: DeploymentTemplate - ): Promise { - return await callWithTelemetryAndErrorHandling('reportDeploymentTemplateErrors', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.suppressIfSuccessful = true; - - // tslint:disable-next-line:no-suspicious-comment - // TODO: Associated parameters - const associatedParameters: DeploymentParameters | undefined = undefined; - return await this.reportDeploymentDocumentErrors(textDocument, deploymentTemplate, associatedParameters); - }); + // Only ask to upgrade once per session per file + const document = editor.document; + const documentPath = document.uri.fsPath; + let queriedToUpdateSchema = this._filesAskedToUpdateSchemaThisSession.has(documentPath); + if (queriedToUpdateSchema) { + return; } - private async reportDeploymentParametersErrors( - textDocument: vscode.TextDocument, - deploymentParameters: DeploymentParameters - ): Promise { - return await callWithTelemetryAndErrorHandling('reportDeploymentParametersErrors', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.suppressIfSuccessful = true; + this._filesAskedToUpdateSchemaThisSession.add(documentPath); - const template = await this.getOrReadAssociatedTemplate(textDocument.uri, Cancellation.cantCancel); - return await this.reportDeploymentDocumentErrors(textDocument, deploymentParameters, template); - }); - } - - private considerQueryingForNewerSchema(editor: vscode.TextEditor, deploymentTemplate: DeploymentTemplate): void { - // Only deal with saved files, because we don't have an accurate - // URI that we can track for unsaved files, and it's a better user experience. - if (editor.document.uri.scheme !== 'file') { - return; - } + const schemaValue: Json.StringValue | undefined = deploymentTemplate.schemaValue; + // tslint:disable-next-line: strict-boolean-expressions + const schemaUri: string | undefined = deploymentTemplate.schemaUri || undefined; + const preferredSchemaUri: string | undefined = schemaUri && getPreferredSchema(schemaUri); + const checkForLatestSchema = !!vscode.workspace.getConfiguration(configPrefix).get(configKeys.checkForLatestSchema); - // Only ask to upgrade once per session per file - const document = editor.document; - const documentPath = document.uri.fsPath; - let queriedToUpdateSchema = this._filesAskedToUpdateSchemaThisSession.has(documentPath); - if (queriedToUpdateSchema) { - return; - } + if (preferredSchemaUri && schemaValue) { + // tslint:disable-next-line: no-floating-promises // Don't wait + callWithTelemetryAndErrorHandling('queryUpdateSchema', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.properties.currentSchema = schemaUri; + actionContext.telemetry.properties.preferredSchema = preferredSchemaUri; + actionContext.telemetry.properties.checkForLatestSchema = String(checkForLatestSchema); - this._filesAskedToUpdateSchemaThisSession.add(documentPath); + if (!checkForLatestSchema) { + return; + } - const schemaValue: Json.StringValue | undefined = deploymentTemplate.schemaValue; - // tslint:disable-next-line: strict-boolean-expressions - const schemaUri: string | undefined = deploymentTemplate.schemaUri || undefined; - const preferredSchemaUri: string | undefined = schemaUri && getPreferredSchema(schemaUri); - const checkForLatestSchema = !!vscode.workspace.getConfiguration(configPrefix).get(configKeys.checkForLatestSchema); + // tslint:disable-next-line: strict-boolean-expressions + const dontAskFiles = ext.context.globalState.get(globalStateKeys.dontAskAboutSchemaFiles) || []; + if (dontAskFiles.includes(documentPath)) { + actionContext.telemetry.properties.isInDontAskList = 'true'; + return; + } - if (preferredSchemaUri && schemaValue) { - // tslint:disable-next-line: no-floating-promises // Don't wait - callWithTelemetryAndErrorHandling('queryUpdateSchema', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.properties.currentSchema = schemaUri; - actionContext.telemetry.properties.preferredSchema = preferredSchemaUri; - actionContext.telemetry.properties.checkForLatestSchema = String(checkForLatestSchema); + const yes: vscode.MessageItem = { title: "Use latest" }; + const notNow: vscode.MessageItem = { title: "Not now" }; + const neverForThisFile: vscode.MessageItem = { title: "Never for this file" }; - if (!checkForLatestSchema) { + const response = await ext.ui.showWarningMessage( + `Would you like to use the latest schema for deployment template "${path.basename(document.uri.path)}" (note: some tools may be unable to process the latest schema)?`, + { + learnMoreLink: "https://aka.ms/vscode-azurearmtools-updateschema" + }, + yes, + notNow, + neverForThisFile + ); + actionContext.telemetry.properties.response = response.title; + + switch (response.title) { + case yes.title: + await this.replaceSchema(document.uri, deploymentTemplate, schemaValue.unquotedValue, preferredSchemaUri); + actionContext.telemetry.properties.replacedSchema = "true"; return; - } - - // tslint:disable-next-line: strict-boolean-expressions - const dontAskFiles = ext.context.globalState.get(globalStateKeys.dontAskAboutSchemaFiles) || []; - if (dontAskFiles.includes(documentPath)) { - actionContext.telemetry.properties.isInDontAskList = 'true'; + case notNow.title: return; - } + case neverForThisFile.title: + dontAskFiles.push(documentPath); + await ext.context.globalState.update(globalStateKeys.dontAskAboutSchemaFiles, dontAskFiles); + break; + default: + assert("queryUseNewerSchema: Unexpected response"); + break; + } + }); + } + } - const yes: vscode.MessageItem = { title: "Use latest" }; - const notNow: vscode.MessageItem = { title: "Not now" }; - const neverForThisFile: vscode.MessageItem = { title: "Never for this file" }; - - const response = await ext.ui.showWarningMessage( - `Would you like to use the latest schema for deployment template "${path.basename(document.uri.path)}" (note: some tools may be unable to process the latest schema)?`, - { - learnMoreLink: "https://aka.ms/vscode-azurearmtools-updateschema" - }, - yes, - notNow, - neverForThisFile - ); - actionContext.telemetry.properties.response = response.title; + private async replaceSchema(uri: vscode.Uri, deploymentTemplate: DeploymentTemplate, previousSchema: string, newSchema: string): Promise { + // Editor might have been closed or tabbed away from, so make sure it's visible + const editor = await vscode.window.showTextDocument(uri); + + // The document might have changed since we asked, so find the $schema again + const currentTemplate = new DeploymentTemplate(editor.document.getText(), editor.document.uri); + const currentSchemaValue: Json.StringValue | undefined = currentTemplate.schemaValue; + if (currentSchemaValue && currentSchemaValue.unquotedValue === previousSchema) { + const range = getVSCodeRangeFromSpan(currentTemplate, currentSchemaValue.unquotedSpan); + await editor.edit(edit => { + // Replace $schema value + edit.replace(range, newSchema); + }); - switch (response.title) { - case yes.title: - await this.replaceSchema(document.uri, deploymentTemplate, schemaValue.unquotedValue, preferredSchemaUri); - actionContext.telemetry.properties.replacedSchema = "true"; - return; - case notNow.title: - return; - case neverForThisFile.title: - dontAskFiles.push(documentPath); - await ext.context.globalState.update(globalStateKeys.dontAskAboutSchemaFiles, dontAskFiles); - break; - default: - assert("queryUseNewerSchema: Unexpected response"); - break; - } - }); - } + // Select what we just replaced + editor.selection = new vscode.Selection(range.start, range.end); + editor.revealRange(range, vscode.TextEditorRevealType.Default); + } else { + throw new Error("The document has changed, the $schema was not replaced."); } + } - private async replaceSchema(uri: vscode.Uri, deploymentTemplate: DeploymentTemplate, previousSchema: string, newSchema: string): Promise { - // Editor might have been closed or tabbed away from, so make sure it's visible - const editor = await vscode.window.showTextDocument(uri); - - // The document might have changed since we asked, so find the $schema again - const currentTemplate = new DeploymentTemplate(editor.document.getText(), editor.document.uri); - const currentSchemaValue: Json.StringValue | undefined = currentTemplate.schemaValue; - if (currentSchemaValue && currentSchemaValue.unquotedValue === previousSchema) { - const range = getVSCodeRangeFromSpan(currentTemplate, currentSchemaValue.unquotedSpan); - await editor.edit(edit => { - // Replace $schema value - edit.replace(range, newSchema); - }); - - // Select what we just replaced - editor.selection = new vscode.Selection(range.start, range.end); - editor.revealRange(range, vscode.TextEditorRevealType.Default); - } else { - throw new Error("The document has changed, the $schema was not replaced."); - } + private getCompletedDiagnostic(): vscode.Diagnostic | undefined { + if (ext.addCompletedDiagnostic) { + // Add a diagnostic to indicate expression validation is done (for testing) + return { + severity: vscode.DiagnosticSeverity.Information, + message: `${expressionsDiagnosticsCompletionMessage}, version ${this._diagnosticsVersion}`, + source: expressionsDiagnosticsSource, + code: "", + range: new vscode.Range(0, 0, 0, 0) + }; + } else { + return undefined; } + } - private getCompletedDiagnostic(): vscode.Diagnostic | undefined { - if (ext.addCompletedDiagnostic) { - // Add a diagnostic to indicate expression validation is done (for testing) - return { - severity: vscode.DiagnosticSeverity.Information, - message: `${expressionsDiagnosticsCompletionMessage}, version ${this._diagnosticsVersion}`, - source: expressionsDiagnosticsSource, - code: "", - range: new vscode.Range(0, 0, 0, 0) - }; - } else { - return undefined; - } + /** + * Hook up events related to template files (as opposed to plain JSON files). This is only called when + * actual template files are open, to avoid slowing performance when simple JSON files are opened. + */ + private ensureDeploymentDocumentEventsHookedUp(): void { + if (this._areDeploymentTemplateEventsHookedUp) { + return; } + this._areDeploymentTemplateEventsHookedUp = true; - /** - * Hook up events related to template files (as opposed to plain JSON files). This is only called when - * actual template files are open, to avoid slowing performance when simple JSON files are opened. - */ - private ensureDeploymentDocumentEventsHookedUp(): void { - if (this._areDeploymentTemplateEventsHookedUp) { - return; - } - this._areDeploymentTemplateEventsHookedUp = true; + vscode.window.onDidChangeTextEditorSelection(this.onTextSelectionChanged, this, ext.context.subscriptions); - vscode.window.onDidChangeTextEditorSelection(this.onTextSelectionChanged, this, ext.context.subscriptions); + vscode.workspace.onDidCloseTextDocument(this.onDocumentClosed, this, ext.context.subscriptions); - vscode.workspace.onDidCloseTextDocument(this.onDocumentClosed, this, ext.context.subscriptions); - - const hoverProvider: vscode.HoverProvider = { - provideHover: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { - return await this.onProvideHover(document, position, token); - } - }; - ext.context.subscriptions.push(vscode.languages.registerHoverProvider(templateDocumentSelector, hoverProvider)); - - // Code actions provider - const codeActionProvider: vscode.CodeActionProvider = { - provideCodeActions: async ( - textDocument: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext, - token: vscode.CancellationToken - ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> => { - return await this.onProvideCodeActions(textDocument, range, context, token); - } - }; - ext.context.subscriptions.push( - vscode.languages.registerCodeActionsProvider( - templateOrParameterDocumentSelector, - codeActionProvider, - { - providedCodeActionKinds: [ - vscode.CodeActionKind.QuickFix - ] - } - )); - - // tslint:disable-next-line:no-suspicious-comment - const completionProvider: vscode.CompletionItemProvider = { - provideCompletionItems: async ( - document: vscode.TextDocument, - position: vscode.Position, - token: vscode.CancellationToken, - context: vscode.CompletionContext - ): Promise => { - return await this.onProvideCompletions(document, position, token); - }, - resolveCompletionItem: (item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.CompletionItem => { - return this.onResolveCompletionItem(item, token); + const hoverProvider: vscode.HoverProvider = { + provideHover: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideHover(document, position, token); + } + }; + ext.context.subscriptions.push(vscode.languages.registerHoverProvider(templateDocumentSelector, hoverProvider)); + + // Code actions provider + const codeActionProvider: vscode.CodeActionProvider = { + provideCodeActions: async ( + textDocument: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> => { + return await this.onProvideCodeActions(textDocument, range, context, token); + } + }; + ext.context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + templateOrParameterDocumentSelector, + codeActionProvider, + { + providedCodeActionKinds: [ + vscode.CodeActionKind.QuickFix + ] } - }; - ext.context.subscriptions.push( - vscode.languages.registerCompletionItemProvider( - templateOrParameterDocumentSelector, - completionProvider, - "'", "[", ".", "(", '"' - )); + )); + + // tslint:disable-next-line:no-suspicious-comment + const completionProvider: vscode.CompletionItemProvider = { + provideCompletionItems: async ( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + context: vscode.CompletionContext + ): Promise => { + return await this.onProvideCompletions(document, position, token); + }, + resolveCompletionItem: (item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.CompletionItem => { + return this.onResolveCompletionItem(item, token); + } + }; + ext.context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + templateOrParameterDocumentSelector, + completionProvider, + "'", "[", ".", "(", '"' + )); + + // tslint:disable-next-line:no-suspicious-comment + const definitionProvider: vscode.DefinitionProvider = { + provideDefinition: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideDefinition(document, position, token); + } + }; + ext.context.subscriptions.push( + vscode.languages.registerDefinitionProvider( + templateOrParameterDocumentSelector, + definitionProvider)); + + const referenceProvider: vscode.ReferenceProvider = { + provideReferences: async (document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise => { + return this.onProvideReferences(document, position, context, token); + } + }; + ext.context.subscriptions.push(vscode.languages.registerReferenceProvider(templateOrParameterDocumentSelector, referenceProvider)); - // tslint:disable-next-line:no-suspicious-comment - const definitionProvider: vscode.DefinitionProvider = { - provideDefinition: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { - return await this.onProvideDefinition(document, position, token); - } - }; - ext.context.subscriptions.push( - vscode.languages.registerDefinitionProvider( - templateOrParameterDocumentSelector, - definitionProvider)); - - const referenceProvider: vscode.ReferenceProvider = { - provideReferences: async (document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise => { - return this.onProvideReferences(document, position, context, token); - } - }; - ext.context.subscriptions.push(vscode.languages.registerReferenceProvider(templateOrParameterDocumentSelector, referenceProvider)); + const signatureHelpProvider: vscode.SignatureHelpProvider = { + provideSignatureHelp: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.onProvideSignatureHelp(document, position, token); + } + }; + ext.context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(templateDocumentSelector, signatureHelpProvider, ",", "(", "\n")); - const signatureHelpProvider: vscode.SignatureHelpProvider = { - provideSignatureHelp: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { - return await this.onProvideSignatureHelp(document, position, token); - } - }; - ext.context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(templateDocumentSelector, signatureHelpProvider, ",", "(", "\n")); - - const renameProvider: vscode.RenameProvider = { - provideRenameEdits: async (document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise => { - return await this.onProvideRename(document, position, newName, token); - }, - prepareRename: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { - return await this.prepareRename(document, position, token); - } - }; - ext.context.subscriptions.push(vscode.languages.registerRenameProvider(templateOrParameterDocumentSelector, renameProvider)); + const renameProvider: vscode.RenameProvider = { + provideRenameEdits: async (document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise => { + return await this.onProvideRename(document, position, newName, token); + }, + prepareRename: async (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise => { + return await this.prepareRename(document, position, token); + } + }; + ext.context.subscriptions.push(vscode.languages.registerRenameProvider(templateOrParameterDocumentSelector, renameProvider)); - // tslint:disable-next-line:no-floating-promises // Don't wait - startArmLanguageServer(); - } + // tslint:disable-next-line:no-floating-promises // Don't wait + startArmLanguageServer(); + } - private async updateEditorState(): Promise { - let show = false; - let isTemplateFile = false; - let templateFileHasParamFile = false; - let isParamFile = false; - let paramFileHasTemplateFile = false; - - try { - const activeDocument = vscode.window.activeTextEditor?.document; - if (activeDocument) { - const deploymentTemplate = this.getOpenedDeploymentDocument(activeDocument); - if (deploymentTemplate instanceof DeploymentTemplate) { - show = true; - isTemplateFile = true; - let statusBarText: string; - - const paramFileUri = this._mapping.getParameterFile(activeDocument.uri); - if (paramFileUri) { - templateFileHasParamFile = true; - const doesParamFileExist = await fse.pathExists(paramFileUri?.fsPath); - statusBarText = `Parameters: ${getFriendlyPathToFile(paramFileUri)}`; - if (!doesParamFileExist) { - statusBarText += " $(error) Not found"; - } - } else { - statusBarText = "Select Parameter File..."; + private async updateEditorState(): Promise { + let show = false; + let isTemplateFile = false; + let templateFileHasParamFile = false; + let isParamFile = false; + let paramFileHasTemplateFile = false; + + try { + const activeDocument = vscode.window.activeTextEditor?.document; + if (activeDocument) { + const deploymentTemplate = this.getOpenedDeploymentDocument(activeDocument); + if (deploymentTemplate instanceof DeploymentTemplate) { + show = true; + isTemplateFile = true; + let statusBarText: string; + + const paramFileUri = this._mapping.getParameterFile(activeDocument.uri); + if (paramFileUri) { + templateFileHasParamFile = true; + const doesParamFileExist = await fse.pathExists(paramFileUri?.fsPath); + statusBarText = `Parameters: ${getFriendlyPathToFile(paramFileUri)}`; + if (!doesParamFileExist) { + statusBarText += " $(error) Not found"; } + } else { + statusBarText = "Select Parameter File..."; + } - this._paramsStatusBarItem.command = "azurerm-vscode-tools.selectParameterFile"; - this._paramsStatusBarItem.text = statusBarText; - } else if (deploymentTemplate instanceof DeploymentParameters) { - show = true; - isParamFile = true; - let statusBarText: string; - - const templateFileUri = this._mapping.getTemplateFile(activeDocument.uri); - if (templateFileUri) { - paramFileHasTemplateFile = true; - const doesTemplateFileExist = await fse.pathExists(templateFileUri?.fsPath); - statusBarText = `Template file: ${getFriendlyPathToFile(templateFileUri)}`; - if (!doesTemplateFileExist) { - statusBarText += " $(error) Not found"; - } - } else { - statusBarText = "No template file selected"; + this._paramsStatusBarItem.command = "azurerm-vscode-tools.selectParameterFile"; + this._paramsStatusBarItem.text = statusBarText; + } else if (deploymentTemplate instanceof DeploymentParameters) { + show = true; + isParamFile = true; + let statusBarText: string; + + const templateFileUri = this._mapping.getTemplateFile(activeDocument.uri); + if (templateFileUri) { + paramFileHasTemplateFile = true; + const doesTemplateFileExist = await fse.pathExists(templateFileUri?.fsPath); + statusBarText = `Template file: ${getFriendlyPathToFile(templateFileUri)}`; + if (!doesTemplateFileExist) { + statusBarText += " $(error) Not found"; } - - this._paramsStatusBarItem.text = statusBarText; + } else { + statusBarText = "No template file selected"; } - } - } finally { - if (show) { - this._paramsStatusBarItem.show(); - } else { - this._paramsStatusBarItem.hide(); - } - // tslint:disable-next-line: no-floating-promises - setParameterFileContext({ - isTemplateFile, - hasParamFile: templateFileHasParamFile, - isParamFile: isParamFile, - hasTemplateFile: paramFileHasTemplateFile - }); + this._paramsStatusBarItem.text = statusBarText; + } + } + } finally { + if (show) { + this._paramsStatusBarItem.show(); + } else { + this._paramsStatusBarItem.hide(); } - } - /** - * Logs telemetry with information about the functions used in a template. Only meaningful if called - * in a relatively stable state, such as after first opening - */ - private logFunctionCounts(deploymentTemplate: DeploymentTemplate): void { - // Don't wait for promise // tslint:disable-next-line: no-floating-promises - callWithTelemetryAndErrorHandling("tle.stats", async (actionContext: IActionContext): Promise => { - actionContext.errorHandling.suppressDisplay = true; - let properties: { - functionCounts?: string; - unrecognized?: string; - incorrectArgs?: string; - } & TelemetryProperties = actionContext.telemetry.properties; - - let issues: language.Issue[] = await deploymentTemplate.getErrors(undefined); - - // Full function counts - const functionCounts: Histogram = deploymentTemplate.getFunctionCounts(); - const functionsData: { [key: string]: number } = {}; - for (const functionName of functionCounts.keys) { - functionsData[functionName] = functionCounts.getCount(functionName); - } - properties.functionCounts = JSON.stringify(functionsData); - - // Missing function names and functions with incorrect number of arguments (useful for knowing - // if our expressionMetadata.json file is up to date) - let unrecognized = new Set(); - let incorrectArgCounts = new Set(); - for (const issue of issues) { - if (issue instanceof UnrecognizedBuiltinFunctionIssue) { - unrecognized.add(issue.functionName); - } else if (issue instanceof IncorrectArgumentsCountIssue) { - // Encode function name as "funcname()[..]" - let encodedName = `${issue.functionName}(${issue.actual})[${issue.minExpected}..${issue.maxExpected}]`; - incorrectArgCounts.add(encodedName); - } - } - properties.unrecognized = AzureRMTools.convertSetToJson(unrecognized); - properties.incorrectArgs = AzureRMTools.convertSetToJson(incorrectArgCounts); + setParameterFileContext({ + isTemplateFile, + hasParamFile: templateFileHasParamFile, + isParamFile: isParamFile, + hasTemplateFile: paramFileHasTemplateFile }); } + } - /** - * Log information about which resource types and apiVersions are being used - */ - private logResourceUsage(deploymentTemplate: DeploymentTemplate): void { - // Don't wait for promise - // tslint:disable-next-line: no-floating-promises - callWithTelemetryAndErrorHandling("schema.stats", async (actionContext: IActionContext): Promise => { - actionContext.errorHandling.suppressDisplay = true; - let properties: { - resourceCounts?: string; - } & TelemetryProperties = actionContext.telemetry.properties; + /** + * Logs telemetry with information about the functions used in a template. Only meaningful if called + * in a relatively stable state, such as after first opening + */ + private logFunctionCounts(deploymentTemplate: DeploymentTemplate): void { + // Don't wait for promise + // tslint:disable-next-line: no-floating-promises + callWithTelemetryAndErrorHandling("tle.stats", async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + let properties: { + functionCounts?: string; + unrecognized?: string; + incorrectArgs?: string; + } & TelemetryProperties = actionContext.telemetry.properties; + + let issues: language.Issue[] = await deploymentTemplate.getErrors(undefined); + + // Full function counts + const functionCounts: Histogram = deploymentTemplate.getFunctionCounts(); + const functionsData: { [key: string]: number } = {}; + for (const functionName of functionCounts.keys) { + functionsData[functionName] = functionCounts.getCount(functionName); + } + properties.functionCounts = JSON.stringify(functionsData); + + // Missing function names and functions with incorrect number of arguments (useful for knowing + // if our expressionMetadata.json file is up to date) + let unrecognized = new Set(); + let incorrectArgCounts = new Set(); + for (const issue of issues) { + if (issue instanceof UnrecognizedBuiltinFunctionIssue) { + unrecognized.add(issue.functionName); + } else if (issue instanceof IncorrectArgumentsCountIssue) { + // Encode function name as "funcname()[..]" + let encodedName = `${issue.functionName}(${issue.actual})[${issue.minExpected}..${issue.maxExpected}]`; + incorrectArgCounts.add(encodedName); + } + } + properties.unrecognized = AzureRMTools.convertSetToJson(unrecognized); + properties.incorrectArgs = AzureRMTools.convertSetToJson(incorrectArgCounts); + }); + } - const resourceCounts: Histogram = deploymentTemplate.getResourceUsage(); - properties.resourceCounts = this.histogramToTelemetryString(resourceCounts); - }); - } + /** + * Log information about which resource types and apiVersions are being used + */ + private logResourceUsage(deploymentTemplate: DeploymentTemplate): void { + // Don't wait for promise + // tslint:disable-next-line: no-floating-promises + callWithTelemetryAndErrorHandling("schema.stats", async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + let properties: { + resourceCounts?: string; + } & TelemetryProperties = actionContext.telemetry.properties; + + const resourceCounts: Histogram = deploymentTemplate.getResourceUsage(); + properties.resourceCounts = this.histogramToTelemetryString(resourceCounts); + }); + } - private histogramToTelemetryString(histogram: Histogram): string { - const data: { [key: string]: number } = {}; - for (const key of histogram.keys) { - data[key] = histogram.getCount(key); - } - return JSON.stringify(data); + private histogramToTelemetryString(histogram: Histogram): string { + const data: { [key: string]: number } = {}; + for (const key of histogram.keys) { + data[key] = histogram.getCount(key); } + return JSON.stringify(data); + } - private static convertSetToJson(s: Set): string { - // tslint:disable-next-line: strict-boolean-expressions - if (!s.size) { - return ""; - } - let array: string[] = []; - for (let item of s) { - array.push(item); - } - return JSON.stringify(array); + private static convertSetToJson(s: Set): string { + // tslint:disable-next-line: strict-boolean-expressions + if (!s.size) { + return ""; } - - private getVSCodeDiagnosticFromIssue(deploymentDocument: DeploymentDocument, issue: language.Issue, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { - const range: vscode.Range = getVSCodeRangeFromSpan(deploymentDocument, issue.span); - const message: string = issue.message; - let diagnostic = new vscode.Diagnostic(range, message, severity); - diagnostic.source = expressionsDiagnosticsSource; - diagnostic.code = ""; - return diagnostic; + let array: string[] = []; + for (let item of s) { + array.push(item); } + return JSON.stringify(array); + } - private closeDeploymentFile(document: vscode.TextDocument): void { - assert(document); - this._diagnosticsCollection.delete(document.uri); - this.setOpenedDeploymentDocument(document.uri, undefined); - } + private getVSCodeDiagnosticFromIssue(deploymentDocument: DeploymentDocument, issue: language.Issue, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { + const range: vscode.Range = getVSCodeRangeFromSpan(deploymentDocument, issue.span); + const message: string = issue.message; + let diagnostic = new vscode.Diagnostic(range, message, severity); + diagnostic.source = expressionsDiagnosticsSource; + diagnostic.code = ""; + return diagnostic; + } - private async onProvideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('Hover', async (actionContext: IActionContext): Promise => { - actionContext.errorHandling.suppressDisplay = true; - const properties = actionContext.telemetry.properties; + private closeDeploymentFile(document: vscode.TextDocument): void { + assert(document); + this._diagnosticsCollection.delete(document.uri); + this.setOpenedDeploymentDocument(document.uri, undefined); + } - const cancel = new Cancellation(token, actionContext); + private async onProvideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Hover', async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + const properties = actionContext.telemetry.properties; - const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(document, cancel); - if (doc) { - const context = doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); - const hoverInfo: Hover.HoverInfo | undefined = context.getHoverInfo(); + const cancel = new Cancellation(token, actionContext); - if (hoverInfo) { - properties.hoverType = hoverInfo.friendlyType; - const hoverRange: vscode.Range = getVSCodeRangeFromSpan(doc, hoverInfo.span); - const hover = new vscode.Hover(hoverInfo.getHoverText(), hoverRange); - return hover; - } + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(document, cancel); + if (doc) { + const context = doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); + const hoverInfo: Hover.HoverInfo | undefined = context.getHoverInfo(); + + if (hoverInfo) { + properties.hoverType = hoverInfo.friendlyType; + const hoverRange: vscode.Range = getVSCodeRangeFromSpan(doc, hoverInfo.span); + const hover = new vscode.Hover(hoverInfo.getHoverText(), hoverRange); + return hover; } + } - return undefined; - }); - } - - private async onProvideCompletions(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('provideCompletionItems', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.suppressIfSuccessful = true; - actionContext.errorHandling.suppressDisplay = true; - - const cancel = new Cancellation(token, actionContext); - - const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); - if (pc) { - const items: Completion.Item[] = pc.getCompletionItems(); - const vsCodeItems = items.map(c => toVsCodeCompletionItem(pc.document, c)); - ext.completionItemsSpy.postCompletionItemsResult(pc.document, items, vsCodeItems); + return undefined; + }); + } - // vscode requires all spans to include the original position and be on the same line, otherwise - // it ignores it. Verify that here. - for (let item of vsCodeItems) { - assert(item.range, "Completion item doesn't have a range"); - assert(item.range?.contains(position), "Completion item range doesn't include cursor"); - assert(item.range?.isSingleLine, "Completion item range must be a single line"); - } - return new vscode.CompletionList(vsCodeItems, true); + private async onProvideCompletions(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('provideCompletionItems', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.suppressIfSuccessful = true; + actionContext.errorHandling.suppressDisplay = true; + + const cancel = new Cancellation(token, actionContext); + + const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); + if (pc) { + const items: Completion.Item[] = pc.getCompletionItems(); + const vsCodeItems = items.map(c => toVsCodeCompletionItem(pc.document, c)); + ext.completionItemsSpy.postCompletionItemsResult(pc.document, items, vsCodeItems); + + // vscode requires all spans to include the original position and be on the same line, otherwise + // it ignores it. Verify that here. + for (let item of vsCodeItems) { + assert(item.range, "Completion item doesn't have a range"); + assert(item.range?.contains(position), "Completion item range doesn't include cursor"); + assert(item.range?.isSingleLine, "Completion item range must be a single line"); } - - return undefined; - }); - } - - private onResolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): vscode.CompletionItem { - ext.completionItemsSpy.postCompletionItemResolution(item); - return item; - } - - /** - * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then - * find the appropriate associated document for it - */ - private async getDeploymentDocAndAssociatedDoc( - textDocument: vscode.TextDocument, - cancel: Cancellation - ): Promise<{ doc?: DeploymentDocument; associatedDoc?: DeploymentDocument }> { - cancel.throwIfCancelled(); - - const doc = this.getOpenedDeploymentDocument(textDocument); - if (!doc) { - // No reason to try reading from disk, if it's not in our opened list, - // it can't be the one in the current text document - return {}; + return new vscode.CompletionList(vsCodeItems, true); } - if (doc instanceof DeploymentTemplate) { - const template: DeploymentTemplate = doc; - // It's a template file - find the associated parameter file, if any - let params: DeploymentParameters | undefined; - const paramsUri: vscode.Uri | undefined = this._mapping.getParameterFile(textDocument.uri); - if (paramsUri) { - params = await this.getOrReadTemplateParameters(paramsUri); - cancel.throwIfCancelled(); - } + return undefined; + }); + } - return { doc: template, associatedDoc: params }; - } else if (doc instanceof DeploymentParameters) { - const params: DeploymentParameters = doc; - // It's a parameter file - find the associated template file, if any - let template: DeploymentTemplate | undefined; - const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(textDocument.uri); - if (templateUri) { - template = await this.getOrReadDeploymentTemplate(templateUri); - cancel.throwIfCancelled(); - } + private onResolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): vscode.CompletionItem { + ext.completionItemsSpy.postCompletionItemResolution(item); + return item; + } - return { doc: params, associatedDoc: template }; - } else { - assert.fail("Unexpected doc type"); - } + /** + * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then + * find the appropriate associated document for it + */ + private async getDeploymentDocAndAssociatedDoc( + textDocument: vscode.TextDocument, + cancel: Cancellation + ): Promise<{ doc?: DeploymentDocument; associatedDoc?: DeploymentDocument }> { + cancel.throwIfCancelled(); + + const doc = this.getOpenedDeploymentDocument(textDocument); + if (!doc) { + // No reason to try reading from disk, if it's not in our opened list, + // it can't be the one in the current text document + return {}; } - /** - * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then - * create the appropriate context for it from the given position - */ - private async getPositionContext(textDocument: vscode.TextDocument, position: vscode.Position, cancel: Cancellation): Promise { - cancel.throwIfCancelled(); + if (doc instanceof DeploymentTemplate) { + const template: DeploymentTemplate = doc; + // It's a template file - find the associated parameter file, if any + let params: DeploymentParameters | undefined; + const paramsUri: vscode.Uri | undefined = this._mapping.getParameterFile(textDocument.uri); + if (paramsUri) { + params = await this.getOrReadTemplateParameters(paramsUri); + cancel.throwIfCancelled(); + } - const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); - if (!doc) { - return undefined; + return { doc: template, associatedDoc: params }; + } else if (doc instanceof DeploymentParameters) { + const params: DeploymentParameters = doc; + // It's a parameter file - find the associated template file, if any + let template: DeploymentTemplate | undefined; + const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(textDocument.uri); + if (templateUri) { + template = await this.getOrReadDeploymentTemplate(templateUri); + cancel.throwIfCancelled(); } - cancel.throwIfCancelled(); - return doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); + return { doc: params, associatedDoc: template }; + } else { + assert.fail("Unexpected doc type"); } + } - /** - * Given a deployment template URI, return the corresponding opened DeploymentTemplate for it. - * If none, create a new one by reading the location from disk - */ - private async getOrReadDeploymentTemplate(uri: vscode.Uri): Promise { - // Is it already opened? - const doc = this.getOpenedDeploymentTemplate(uri); - if (doc) { - return doc; - } + /** + * Given a document, get a DeploymentTemplate or DeploymentParameters instance from it, and then + * create the appropriate context for it from the given position + */ + private async getPositionContext(textDocument: vscode.TextDocument, position: vscode.Position, cancel: Cancellation): Promise { + cancel.throwIfCancelled(); - // Nope, have to read it from disk - const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); - return new DeploymentTemplate(contents, uri); + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); + if (!doc) { + return undefined; } - /** - * Given a parameter file URI, return the corresponding opened DeploymentParameters for it. - * If none, create a new one by reading the location from disk - */ - private async getOrReadTemplateParameters(uri: vscode.Uri): Promise { - // Is it already opened? - const doc = this.getOpenedDeploymentParameters(uri); - if (doc) { - return doc; - } + cancel.throwIfCancelled(); + return doc.getContextFromDocumentLineAndColumnIndexes(position.line, position.character, associatedDoc); + } - // Nope, have to read it from disk - const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); - return new DeploymentParameters(contents, uri); + /** + * Given a deployment template URI, return the corresponding opened DeploymentTemplate for it. + * If none, create a new one by reading the location from disk + */ + private async getOrReadDeploymentTemplate(uri: vscode.Uri): Promise { + // Is it already opened? + const doc = this.getOpenedDeploymentTemplate(uri); + if (doc) { + return doc; } - private async getOrReadAssociatedTemplate(parameterFileUri: vscode.Uri, cancel: Cancellation): Promise { - const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(parameterFileUri); - if (templateUri) { - const template = await this.getOrReadDeploymentTemplate(templateUri); - cancel.throwIfCancelled(); - return template; - } + // Nope, have to read it from disk + const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); + return new DeploymentTemplate(contents, uri); + } - return undefined; + /** + * Given a parameter file URI, return the corresponding opened DeploymentParameters for it. + * If none, create a new one by reading the location from disk + */ + private async getOrReadTemplateParameters(uri: vscode.Uri): Promise { + // Is it already opened? + const doc = this.getOpenedDeploymentParameters(uri); + if (doc) { + return doc; } - private getDocTypeForTelemetry(doc: DeploymentDocument): string { - if (doc instanceof DeploymentTemplate) { - return "template"; - } else if (doc instanceof DeploymentParameters) { - return "parameters"; - } else { - assert.fail("Unexpected doc type"); - } + // Nope, have to read it from disk + const contents = (await fse.readFile(uri.fsPath, { encoding: 'utf8' })).toString(); + return new DeploymentParameters(contents, uri); + } + + private async getOrReadAssociatedTemplate(parameterFileUri: vscode.Uri, cancel: Cancellation): Promise { + const templateUri: vscode.Uri | undefined = this._mapping.getTemplateFile(parameterFileUri); + if (templateUri) { + const template = await this.getOrReadDeploymentTemplate(templateUri); + cancel.throwIfCancelled(); + return template; } - private async onProvideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('Go To Definition', async (actionContext: IActionContext): Promise => { - const cancel = new Cancellation(token, actionContext); - const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); + return undefined; + } - if (pc) { - let properties = actionContext.telemetry.properties; - actionContext.errorHandling.suppressDisplay = true; - properties.docType = this.getDocTypeForTelemetry(pc.document); + private getDocTypeForTelemetry(doc: DeploymentDocument): string { + if (doc instanceof DeploymentTemplate) { + return "template"; + } else if (doc instanceof DeploymentParameters) { + return "parameters"; + } else { + assert.fail("Unexpected doc type"); + } + } - const refInfo = pc.getReferenceSiteInfo(false); - if (refInfo && refInfo.definition.nameValue) { - properties.definitionType = refInfo.definition.definitionKind; + private async onProvideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Go To Definition', async (actionContext: IActionContext): Promise => { + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(document, position, cancel); - return new vscode.Location( - refInfo.definitionDocument.documentId, - getVSCodeRangeFromSpan(refInfo.definitionDocument, refInfo.definition.nameValue.span) - ); - } + if (pc) { + let properties = actionContext.telemetry.properties; + actionContext.errorHandling.suppressDisplay = true; + properties.docType = this.getDocTypeForTelemetry(pc.document); - return undefined; - } - }); - } + const refInfo = pc.getReferenceSiteInfo(false); + if (refInfo && refInfo.definition.nameValue) { + properties.definitionType = refInfo.definition.definitionKind; - private async onProvideReferences(textDocument: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('Find References', async (actionContext: IActionContext): Promise => { - const cancel = new Cancellation(token, actionContext); - const results: vscode.Location[] = []; - const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); - if (pc) { - const references: ReferenceList | undefined = pc.getReferences(); - if (references && references.length > 0) { - actionContext.telemetry.properties.referenceType = references.kind; - - for (const ref of references.references) { - const locationUri: vscode.Uri = ref.document.documentId; - const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); - results.push(new vscode.Location(locationUri, referenceRange)); - } - } + return new vscode.Location( + refInfo.definitionDocument.documentId, + getVSCodeRangeFromSpan(refInfo.definitionDocument, refInfo.definition.nameValue.span) + ); } - return results; - }); - } - - /** - * Provide commands for the given document and range. - * - * @param textDocument The document in which the command was invoked. - * @param range The selector or range for which the command was invoked. This will always be a selection if - * there is a currently active editor. - * @param context Context carrying additional information. - * @param token A cancellation token. - * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be - * signaled by returning `undefined`, `null`, or an empty array. - */ - private async onProvideCodeActions( - textDocument: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext, - token: vscode.CancellationToken - ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> { - return await callWithTelemetryAndErrorHandling('Provide code actions', async (actionContext: IActionContext): Promise<(vscode.Command | vscode.CodeAction)[]> => { - actionContext.errorHandling.suppressDisplay = true; - const cancel = new Cancellation(token, actionContext); + return undefined; + } + }); + } - const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); - if (doc) { - return await doc.getCodeActions(associatedDoc, range, context); + private async onProvideReferences(textDocument: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Find References', async (actionContext: IActionContext): Promise => { + const cancel = new Cancellation(token, actionContext); + const results: vscode.Location[] = []; + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (pc) { + const references: ReferenceList | undefined = pc.getReferences(); + if (references && references.length > 0) { + actionContext.telemetry.properties.referenceType = references.kind; + + for (const ref of references.references) { + const locationUri: vscode.Uri = ref.document.documentId; + const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); + results.push(new vscode.Location(locationUri, referenceRange)); + } } + } - return []; - }); - } + return results; + }); + } - private async onProvideSignatureHelp(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('provideSignatureHelp', async (actionContext: IActionContext): Promise => { - actionContext.errorHandling.suppressDisplay = true; + /** + * Provide commands for the given document and range. + * + * @param textDocument The document in which the command was invoked. + * @param range The selector or range for which the command was invoked. This will always be a selection if + * there is a currently active editor. + * @param context Context carrying additional information. + * @param token A cancellation token. + * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty array. + */ + private async onProvideCodeActions( + textDocument: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): Promise<(vscode.Command | vscode.CodeAction)[] | undefined> { + return await callWithTelemetryAndErrorHandling('Provide code actions', async (actionContext: IActionContext): Promise<(vscode.Command | vscode.CodeAction)[]> => { + actionContext.errorHandling.suppressDisplay = true; + const cancel = new Cancellation(token, actionContext); - const cancel = new Cancellation(token, actionContext); - const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); - if (pc) { - let functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); - let signatureHelp: vscode.SignatureHelp | undefined; - - if (functionSignatureHelp) { - const signatureInformation = new vscode.SignatureInformation(functionSignatureHelp.functionMetadata.usage, functionSignatureHelp.functionMetadata.description); - signatureInformation.parameters = []; - for (const param of functionSignatureHelp.functionMetadata.parameters) { - // Parameter label needs to be in the exact same format as in the function usage (including type, if you want it to get highlighted with the parameter name) - const paramUsage = getFunctionParamUsage(param.name, param.type); - const paramDocumentation = ""; - signatureInformation.parameters.push(new vscode.ParameterInformation(paramUsage, paramDocumentation)); - } + const { doc, associatedDoc } = await this.getDeploymentDocAndAssociatedDoc(textDocument, cancel); + if (doc) { + return await doc.getCodeActions(associatedDoc, range, context); + } + + return []; + }); + } - signatureHelp = new vscode.SignatureHelp(); - signatureHelp.activeParameter = functionSignatureHelp.activeParameterIndex; - signatureHelp.activeSignature = 0; - signatureHelp.signatures = [signatureInformation]; + private async onProvideSignatureHelp(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('provideSignatureHelp', async (actionContext: IActionContext): Promise => { + actionContext.errorHandling.suppressDisplay = true; + + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (pc) { + let functionSignatureHelp: TLE.FunctionSignatureHelp | undefined = pc.getSignatureHelp(); + let signatureHelp: vscode.SignatureHelp | undefined; + + if (functionSignatureHelp) { + const signatureInformation = new vscode.SignatureInformation(functionSignatureHelp.functionMetadata.usage, functionSignatureHelp.functionMetadata.description); + signatureInformation.parameters = []; + for (const param of functionSignatureHelp.functionMetadata.parameters) { + // Parameter label needs to be in the exact same format as in the function usage (including type, if you want it to get highlighted with the parameter name) + const paramUsage = getFunctionParamUsage(param.name, param.type); + const paramDocumentation = ""; + signatureInformation.parameters.push(new vscode.ParameterInformation(paramUsage, paramDocumentation)); } - return signatureHelp; + signatureHelp = new vscode.SignatureHelp(); + signatureHelp.activeParameter = functionSignatureHelp.activeParameterIndex; + signatureHelp.activeSignature = 0; + signatureHelp.signatures = [signatureInformation]; } - return undefined; - }); - } - - /** - * Optional function for resolving and validating a position *before* running rename. The result can - * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol - * which is being renamed - when omitted the text in the returned range is used. - * - * *Note: * This function should throw an error or return a rejected thenable when the provided location - * doesn't allow for a rename. - * - * @param textDocument The document in which rename will be invoked. - * @param position The position at which rename will be invoked. - * @param token A cancellation token. - * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. - */ - private async prepareRename(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('PrepareRename', async (actionContext) => { - actionContext.errorHandling.rethrow = true; - - const cancel = new Cancellation(token, actionContext); - const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); - if (!token.isCancellationRequested && pc) { - // Make sure the kind of item being renamed is valid - const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); - if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { - actionContext.errorHandling.suppressDisplay = true; - throw new Error("Built-in functions cannot be renamed."); - } + return signatureHelp; + } - if (referenceSiteInfo) { - // Get the correct span to replace. In particular, this fixes the fact that in JSON the rename - // dialog tends to pick up the entire string of a params/var name, along with quotation marks, - // but we want just the unquoted string - return getVSCodeRangeFromSpan(pc.document, referenceSiteInfo.referenceSpan); - } + return undefined; + }); + } + /** + * Optional function for resolving and validating a position *before* running rename. The result can + * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol + * which is being renamed - when omitted the text in the returned range is used. + * + * *Note: * This function should throw an error or return a rejected thenable when the provided location + * doesn't allow for a rename. + * + * @param textDocument The document in which rename will be invoked. + * @param position The position at which rename will be invoked. + * @param token A cancellation token. + * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. + */ + private async prepareRename(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('PrepareRename', async (actionContext) => { + actionContext.errorHandling.rethrow = true; + + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (!token.isCancellationRequested && pc) { + // Make sure the kind of item being renamed is valid + const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); + if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { actionContext.errorHandling.suppressDisplay = true; - throw new Error(invalidRenameError); + throw new Error("Built-in functions cannot be renamed."); } - return undefined; - }); - } + if (referenceSiteInfo) { + // Get the correct span to replace. In particular, this fixes the fact that in JSON the rename + // dialog tends to pick up the entire string of a params/var name, along with quotation marks, + // but we want just the unquoted string + return getVSCodeRangeFromSpan(pc.document, referenceSiteInfo.referenceSpan); + } - private async onProvideRename(textDocument: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { - return await callWithTelemetryAndErrorHandling('Rename', async (actionContext) => { - actionContext.errorHandling.rethrow = true; - - const cancel = new Cancellation(token, actionContext); - const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); - if (!token.isCancellationRequested && pc) { - // Make sure the kind of item being renamed is valid - const result: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); - if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { - throw new Error("Built-in functions cannot be renamed."); - } + actionContext.errorHandling.suppressDisplay = true; + throw new Error(invalidRenameError); + } - const referenceList: ReferenceList | undefined = pc.getReferences(); - if (referenceList) { - // When trying to rename a parameter or variable reference inside of a TLE, the - // textbox that pops up when you press F2 contains more than just the variable - // or parameter name. This next section of code parses out just the variable or - // parameter name. - // For instance, it might provide the textbox with the value "[parameters('location')]" - // We need to pull out the parameter name "location" (no quotes) from that - const firstSingleQuoteIndex: number = newName.indexOf("'"); - if (firstSingleQuoteIndex >= 0) { - const secondSingleQuoteIndex: number = newName.indexOf("'", firstSingleQuoteIndex + 1); - if (secondSingleQuoteIndex >= 0) { - newName = newName.substring(firstSingleQuoteIndex + 1, secondSingleQuoteIndex); - } else { - newName = newName.substring(firstSingleQuoteIndex + 1); - } - } + return undefined; + }); + } - // When trying to rename a parameter or variable definition, the textbox provided by vscode to - // the user is contained in double quotes. Remove those. - newName = newName.replace(/^"(.*)"$/, '$1'); + private async onProvideRename(textDocument: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { + return await callWithTelemetryAndErrorHandling('Rename', async (actionContext) => { + actionContext.errorHandling.rethrow = true; + + const cancel = new Cancellation(token, actionContext); + const pc: PositionContext | undefined = await this.getPositionContext(textDocument, position, cancel); + if (!token.isCancellationRequested && pc) { + // Make sure the kind of item being renamed is valid + const result: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const referenceSiteInfo: IReferenceSite | undefined = pc.getReferenceSiteInfo(true); + if (referenceSiteInfo && referenceSiteInfo.definition.definitionKind === DefinitionKind.BuiltinFunction) { + throw new Error("Built-in functions cannot be renamed."); + } - for (const ref of referenceList.references) { - const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); - result.replace(ref.document.documentId, referenceRange, newName); + const referenceList: ReferenceList | undefined = pc.getReferences(); + if (referenceList) { + // When trying to rename a parameter or variable reference inside of a TLE, the + // textbox that pops up when you press F2 contains more than just the variable + // or parameter name. This next section of code parses out just the variable or + // parameter name. + // For instance, it might provide the textbox with the value "[parameters('location')]" + // We need to pull out the parameter name "location" (no quotes) from that + const firstSingleQuoteIndex: number = newName.indexOf("'"); + if (firstSingleQuoteIndex >= 0) { + const secondSingleQuoteIndex: number = newName.indexOf("'", firstSingleQuoteIndex + 1); + if (secondSingleQuoteIndex >= 0) { + newName = newName.substring(firstSingleQuoteIndex + 1, secondSingleQuoteIndex); + } else { + newName = newName.substring(firstSingleQuoteIndex + 1); } - } else { - throw new Error(invalidRenameError); } - return result; - } - }); - } - - private onActiveTextEditorChanged(editor: vscode.TextEditor | undefined): void { - callWithTelemetryAndErrorHandlingSync('onActiveTextEditorChanged', (actionContext: IActionContext): void => { - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.errorHandling.suppressDisplay = true; - actionContext.telemetry.suppressIfSuccessful = true; + // When trying to rename a parameter or variable definition, the textbox provided by vscode to + // the user is contained in double quotes. Remove those. + newName = newName.replace(/^"(.*)"$/, '$1'); - let activeDocument: vscode.TextDocument | undefined = editor?.document; - if (activeDocument) { - if (!this.getOpenedDeploymentDocument(activeDocument)) { - this.updateOpenedDocument(activeDocument); + for (const ref of referenceList.references) { + const referenceRange: vscode.Range = getVSCodeRangeFromSpan(ref.document, ref.span); + result.replace(ref.document.documentId, referenceRange, newName); } + } else { + throw new Error(invalidRenameError); } - // tslint:disable-next-line: no-floating-promises - this.updateEditorState(); - }); - } + return result; + } + }); + } - private async onTextSelectionChanged(): Promise { - await callWithTelemetryAndErrorHandling('onTextSelectionChanged', async (actionContext: IActionContext): Promise => { - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.errorHandling.suppressDisplay = true; - actionContext.telemetry.suppressIfSuccessful = true; - - let editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (editor) { - let position = editor.selection.anchor; - let pc: PositionContext | undefined = - await this.getPositionContext(editor.document, position, Cancellation.cantCancel); - if (pc && pc instanceof TemplatePositionContext) { - let tleBraceHighlightIndexes: number[] = TLE.BraceHighlighter.getHighlightCharacterIndexes(pc); - - let braceHighlightRanges: vscode.Range[] = []; - for (let tleHighlightIndex of tleBraceHighlightIndexes) { - const highlightSpan = new language.Span(tleHighlightIndex + pc.jsonTokenStartIndex, 1); - braceHighlightRanges.push(getVSCodeRangeFromSpan(pc.document, highlightSpan)); - } + private onActiveTextEditorChanged(editor: vscode.TextEditor | undefined): void { + callWithTelemetryAndErrorHandlingSync('onActiveTextEditorChanged', (actionContext: IActionContext): void => { + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.errorHandling.suppressDisplay = true; + actionContext.telemetry.suppressIfSuccessful = true; - editor.setDecorations(this._braceHighlightDecorationType, braceHighlightRanges); + let activeDocument: vscode.TextDocument | undefined = editor?.document; + if (activeDocument) { + if (!this.getOpenedDeploymentDocument(activeDocument)) { + this.updateOpenedDocument(activeDocument); + } + } + + // tslint:disable-next-line: no-floating-promises + this.updateEditorState(); + }); + } + + private async onTextSelectionChanged(): Promise { + await callWithTelemetryAndErrorHandling('onTextSelectionChanged', async (actionContext: IActionContext): Promise => { + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.errorHandling.suppressDisplay = true; + actionContext.telemetry.suppressIfSuccessful = true; + + let editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (editor) { + let position = editor.selection.anchor; + let pc: PositionContext | undefined = + await this.getPositionContext(editor.document, position, Cancellation.cantCancel); + if (pc && pc instanceof TemplatePositionContext) { + let tleBraceHighlightIndexes: number[] = TLE.BraceHighlighter.getHighlightCharacterIndexes(pc); + + let braceHighlightRanges: vscode.Range[] = []; + for (let tleHighlightIndex of tleBraceHighlightIndexes) { + const highlightSpan = new language.Span(tleHighlightIndex + pc.jsonTokenStartIndex, 1); + braceHighlightRanges.push(getVSCodeRangeFromSpan(pc.document, highlightSpan)); } + + editor.setDecorations(this._braceHighlightDecorationType, braceHighlightRanges); } - }); - } + } + }); + } - private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void { - this.updateOpenedDocument(event.document); - } + private onDocumentChanged(event: vscode.TextDocumentChangeEvent): void { + this.updateOpenedDocument(event.document); + } - private onDocumentOpened(openedDocument: vscode.TextDocument): void { - this.updateOpenedDocument(openedDocument); - } + private onDocumentOpened(openedDocument: vscode.TextDocument): void { + this.updateOpenedDocument(openedDocument); + } - private onDocumentClosed(closedDocument: vscode.TextDocument): void { - callWithTelemetryAndErrorHandlingSync('onDocumentClosed', (actionContext: IActionContext): void => { - actionContext.telemetry.properties.isActivationEvent = 'true'; - actionContext.telemetry.suppressIfSuccessful = true; - actionContext.errorHandling.suppressDisplay = true; + private onDocumentClosed(closedDocument: vscode.TextDocument): void { + callWithTelemetryAndErrorHandlingSync('onDocumentClosed', (actionContext: IActionContext): void => { + actionContext.telemetry.properties.isActivationEvent = 'true'; + actionContext.telemetry.suppressIfSuccessful = true; + actionContext.errorHandling.suppressDisplay = true; - this.closeDeploymentFile(closedDocument); - }); - } - } \ No newline at end of file + this.closeDeploymentFile(closedDocument); + }); + } +} From bb584cccf0745c2ea62442d846bfcd16d3f8d991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 20:46:01 +0200 Subject: [PATCH 45/61] Fixed white space changes in AzureRMTools.ts --- src/AzureRMTools.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 068e3a094..00cc59dcc 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.md in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ // tslint:disable:promise-function-async max-line-length // Grandfathered in From 8e04030f099f960e489cb2caac24893346746173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 12 Apr 2020 20:55:53 +0200 Subject: [PATCH 46/61] Fix lint error --- test/functional/insertItem.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 05a584722..434c4c355 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -94,8 +94,8 @@ suite("InsertItem", async (): Promise => { const totallyEmptyTemplate = `{}`; - async function doTestInsertItem(startTemplate: string, expectedTemplate: string, type: SortType, showInputBox: string[] = [], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise { - await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, type, editor), showInputBox, textToInsert, ignoreWhiteSpace); + async function doTestInsertItem(startTemplate: string, expectedTemplate: string, sortType: SortType, showInputBox: string[] = [], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise { + await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, sortType, editor), showInputBox, textToInsert, ignoreWhiteSpace); } suite("Variables", async () => { From 9842551ec9f0c79c75bab782feb3cfe82000bf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 18 Apr 2020 00:11:22 +0200 Subject: [PATCH 47/61] Trying commenting failing test --- test/functional/insertItem.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 434c4c355..8e0b440a6 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -184,14 +184,14 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, SortType.Resources, ["Application Security Group"], '', true); }); - suite("Resource snippets", async () => { - test("Verify all snippets used by Insert Resource", async () => { - let insertItem = new InsertItem(new MockUserInput([])); - for (const snippet of insertItem.getResourceSnippets()) { - await testResourceSnippet(snippet.label); - } - }); - }); + // suite("Resource snippets", async () => { + // test("Verify all snippets used by Insert Resource", async () => { + // let insertItem = new InsertItem(new MockUserInput([])); + // for (const snippet of insertItem.getResourceSnippets()) { + // await testResourceSnippet(snippet.label); + // } + // }); + // }); }); suite("Functions", async () => { From 519939ccb56e1352127dae4a21129dee6d995900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 18 Apr 2020 00:22:35 +0200 Subject: [PATCH 48/61] Testing resource snippets used by Insert Resource in multiple tests --- extension.bundle.ts | 2 +- src/insertItem.ts | 145 +++++++++++++++-------------- test/functional/insertItem.test.ts | 17 ++-- 3 files changed, 82 insertions(+), 82 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index 0bec726c3..711281b6a 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -45,7 +45,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 { getResourceSnippets, InsertItem } from "./src/insertItem"; export { IParameterDefinition } from "./src/IParameterDefinition"; export * from "./src/Language"; export { LanguageServerState } from "./src/languageclient/startArmLanguageServer"; diff --git a/src/insertItem.ts b/src/insertItem.ts index d89ad6589..638bba757 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -38,6 +38,78 @@ export function getItemType(): QuickPickItem[] { return items; } +export function getResourceSnippets(): vscode.QuickPickItem[] { + let items: vscode.QuickPickItem[] = []; + items.push(getQuickPickItem("Nested Deployment")); + items.push(getQuickPickItem("App Service Plan (Server Farm)")); + items.push(getQuickPickItem("Application Insights for Web Apps")); + items.push(getQuickPickItem("Application Security Group")); + items.push(getQuickPickItem("Automation Account")); + items.push(getQuickPickItem("Automation Certificate")); + items.push(getQuickPickItem("Automation Credential")); + items.push(getQuickPickItem("Automation Job Schedule")); + items.push(getQuickPickItem("Automation Runbook")); + items.push(getQuickPickItem("Automation Schedule")); + items.push(getQuickPickItem("Automation Variable")); + items.push(getQuickPickItem("Automation Module")); + items.push(getQuickPickItem("Availability Set")); + items.push(getQuickPickItem("Azure Firewall")); + items.push(getQuickPickItem("Container Group")); + items.push(getQuickPickItem("Container Registry")); + items.push(getQuickPickItem("Cosmos DB Database Account")); + items.push(getQuickPickItem("Cosmos DB SQL Database")); + items.push(getQuickPickItem("Cosmos DB Mongo Database")); + items.push(getQuickPickItem("Cosmos DB Gremlin Database")); + items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); + items.push(getQuickPickItem("Cosmos DB Cassandra Table")); + items.push(getQuickPickItem("Cosmos DB SQL Container")); + items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); + items.push(getQuickPickItem("Cosmos DB Table Storage Table")); + items.push(getQuickPickItem("Data Lake Store Account")); + items.push(getQuickPickItem("DNS Record")); + items.push(getQuickPickItem("DNS Zone")); + items.push(getQuickPickItem("Function")); + items.push(getQuickPickItem("KeyVault")); + items.push(getQuickPickItem("KeyVault Secret")); + items.push(getQuickPickItem("Kubernetes Service Cluster")); + items.push(getQuickPickItem("Linux VM Custom Script")); + items.push(getQuickPickItem("Load Balancer External")); + items.push(getQuickPickItem("Load Balancer Internal")); + items.push(getQuickPickItem("Log Analytics Solution")); + items.push(getQuickPickItem("Log Analytics Workspace")); + items.push(getQuickPickItem("Logic App")); + items.push(getQuickPickItem("Logic App Connector")); + items.push(getQuickPickItem("Managed Identity (User Assigned)")); + items.push(getQuickPickItem("Media Services")); + items.push(getQuickPickItem("MySQL Database")); + items.push(getQuickPickItem("Network Interface")); + items.push(getQuickPickItem("Network Security Group")); + items.push(getQuickPickItem("Network Security Group Rule")); + items.push(getQuickPickItem("Public IP Address")); + items.push(getQuickPickItem("Public IP Prefix")); + items.push(getQuickPickItem("Recovery Service Vault")); + items.push(getQuickPickItem("Redis Cache")); + items.push(getQuickPickItem("Route Table")); + items.push(getQuickPickItem("Route Table Route")); + items.push(getQuickPickItem("SQL Database")); + items.push(getQuickPickItem("SQL Database Import")); + items.push(getQuickPickItem("SQL Server")); + items.push(getQuickPickItem("Storage Account")); + items.push(getQuickPickItem("Traffic Manager Profile")); + items.push(getQuickPickItem("Ubuntu Virtual Machine")); + items.push(getQuickPickItem("Virtual Network")); + items.push(getQuickPickItem("VPN Local Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Gateway")); + items.push(getQuickPickItem("VPN Virtual Network Connection")); + items.push(getQuickPickItem("Web App")); + items.push(getQuickPickItem("Web Deploy for Web App")); + items.push(getQuickPickItem("Windows Virtual Machine")); + items.push(getQuickPickItem("Windows VM Custom Script")); + items.push(getQuickPickItem("Windows VM Diagnostics Extension")); + items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); + return items; +} + export function getQuickPickItem(label: string): vscode.QuickPickItem { return { label: label }; } @@ -57,77 +129,6 @@ export class InsertItem { constructor(ui: IAzureUserInput) { this.ui = ui; } - public getResourceSnippets(): vscode.QuickPickItem[] { - let items: vscode.QuickPickItem[] = []; - items.push(getQuickPickItem("Nested Deployment")); - items.push(getQuickPickItem("App Service Plan (Server Farm)")); - items.push(getQuickPickItem("Application Insights for Web Apps")); - items.push(getQuickPickItem("Application Security Group")); - items.push(getQuickPickItem("Automation Account")); - items.push(getQuickPickItem("Automation Certificate")); - items.push(getQuickPickItem("Automation Credential")); - items.push(getQuickPickItem("Automation Job Schedule")); - items.push(getQuickPickItem("Automation Runbook")); - items.push(getQuickPickItem("Automation Schedule")); - items.push(getQuickPickItem("Automation Variable")); - items.push(getQuickPickItem("Automation Module")); - items.push(getQuickPickItem("Availability Set")); - items.push(getQuickPickItem("Azure Firewall")); - items.push(getQuickPickItem("Container Group")); - items.push(getQuickPickItem("Container Registry")); - items.push(getQuickPickItem("Cosmos DB Database Account")); - items.push(getQuickPickItem("Cosmos DB SQL Database")); - items.push(getQuickPickItem("Cosmos DB Mongo Database")); - items.push(getQuickPickItem("Cosmos DB Gremlin Database")); - items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); - items.push(getQuickPickItem("Cosmos DB Cassandra Table")); - items.push(getQuickPickItem("Cosmos DB SQL Container")); - items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); - items.push(getQuickPickItem("Cosmos DB Table Storage Table")); - items.push(getQuickPickItem("Data Lake Store Account")); - items.push(getQuickPickItem("DNS Record")); - items.push(getQuickPickItem("DNS Zone")); - items.push(getQuickPickItem("Function")); - items.push(getQuickPickItem("KeyVault")); - items.push(getQuickPickItem("KeyVault Secret")); - items.push(getQuickPickItem("Kubernetes Service Cluster")); - items.push(getQuickPickItem("Linux VM Custom Script")); - items.push(getQuickPickItem("Load Balancer External")); - items.push(getQuickPickItem("Load Balancer Internal")); - items.push(getQuickPickItem("Log Analytics Solution")); - items.push(getQuickPickItem("Log Analytics Workspace")); - items.push(getQuickPickItem("Logic App")); - items.push(getQuickPickItem("Logic App Connector")); - items.push(getQuickPickItem("Managed Identity (User Assigned)")); - items.push(getQuickPickItem("Media Services")); - items.push(getQuickPickItem("MySQL Database")); - items.push(getQuickPickItem("Network Interface")); - items.push(getQuickPickItem("Network Security Group")); - items.push(getQuickPickItem("Network Security Group Rule")); - items.push(getQuickPickItem("Public IP Address")); - items.push(getQuickPickItem("Public IP Prefix")); - items.push(getQuickPickItem("Recovery Service Vault")); - items.push(getQuickPickItem("Redis Cache")); - items.push(getQuickPickItem("Route Table")); - items.push(getQuickPickItem("Route Table Route")); - items.push(getQuickPickItem("SQL Database")); - items.push(getQuickPickItem("SQL Database Import")); - items.push(getQuickPickItem("SQL Server")); - items.push(getQuickPickItem("Storage Account")); - items.push(getQuickPickItem("Traffic Manager Profile")); - items.push(getQuickPickItem("Ubuntu Virtual Machine")); - items.push(getQuickPickItem("Virtual Network")); - items.push(getQuickPickItem("VPN Local Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Connection")); - items.push(getQuickPickItem("Web App")); - items.push(getQuickPickItem("Web Deploy for Web App")); - items.push(getQuickPickItem("Windows Virtual Machine")); - items.push(getQuickPickItem("Windows VM Custom Script")); - items.push(getQuickPickItem("Windows VM Diagnostics Extension")); - items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); - return items; - } public async insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { if (!template) { @@ -331,7 +332,7 @@ export class InsertItem { text = `,\r\n\t\t`; } } - const resource = await this.ui.showQuickPick(this.getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); + const resource = await this.ui.showQuickPick(getResourceSnippets(), { placeHolder: 'What resource do you want to insert?' }); await this.insertText(textEditor, index, text, false); let range = new vscode.Range(textEditor.document.positionAt(index), textEditor.document.positionAt(index + this.formatText(text, textEditor).length)); let insertedText = textEditor.document.getText(range); diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 8e0b440a6..924bf5c90 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports import { commands, window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; -import { DeploymentTemplate, InsertItem, SortType } from '../../extension.bundle'; +import { DeploymentTemplate, getResourceSnippets, InsertItem, SortType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { @@ -184,14 +184,13 @@ suite("InsertItem", async (): Promise => { await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, SortType.Resources, ["Application Security Group"], '', true); }); - // suite("Resource snippets", async () => { - // test("Verify all snippets used by Insert Resource", async () => { - // let insertItem = new InsertItem(new MockUserInput([])); - // for (const snippet of insertItem.getResourceSnippets()) { - // await testResourceSnippet(snippet.label); - // } - // }); - // }); + suite("Resource snippets", async () => { + for (const snippet of getResourceSnippets()) { + test(snippet.label, async () => { + await testResourceSnippet(snippet.label); + }); + } + }); }); suite("Functions", async () => { From d776670ce4440e0c426f18e250974e84c6890b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 25 Apr 2020 17:20:22 +0200 Subject: [PATCH 49/61] Changes based on feedback on PR --- icons/insertItemDark.svg | 3 + icons/insertItemLight.svg | 3 + icons/sortDark.svg | 49 ++++++++++++++++ icons/sortLight.svg | 49 ++++++++++++++++ package.json | 114 +++++++++++++++++++++++++++----------- src/insertItem.ts | 4 +- 6 files changed, 188 insertions(+), 34 deletions(-) create mode 100644 icons/insertItemDark.svg create mode 100644 icons/insertItemLight.svg create mode 100644 icons/sortDark.svg create mode 100644 icons/sortLight.svg 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 eec9bef60..b6cfcfa72 100644 --- a/package.json +++ b/package.json @@ -179,32 +179,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 =============", @@ -246,32 +270,56 @@ { "category": "Azure Resource Manager Tools", "title": "Insert Item...", - "command": "azurerm-vscode-tools.insertItem" + "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" + "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" + "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" + "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" + "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" + "command": "azurerm-vscode-tools.insertOutput", + "icon": { + "light": "icons/insertItemLight.svg", + "dark": "icons/insertItemDark.svg" + } } ], "menus": { @@ -355,64 +403,54 @@ "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", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.sortOutputs", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.sortParameters", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.sortResources", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == resources", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.sortVariables", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", - "group": "arm-template" - }, - { - "command": "azurerm-vscode-tools.insertItem", - "when": "azurerm-vscode-tools.template-outline.active == true", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.insertParameter", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.insertVariable", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.insertOutput", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.insertFunction", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == functions", - "group": "arm-template" + "group": "inline" }, { "command": "azurerm-vscode-tools.insertResource", "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == resources", - "group": "arm-template" + "group": "inline" } ], "editor/title": [ @@ -456,6 +494,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": "azurerm-vscode-tools.template-outline.active == true", + "group": "navigation@1" + }, + { + "command": "azurerm-vscode-tools.sortTemplate", + "when": "azurerm-vscode-tools.template-outline.active == true", + "group": "navigation@2" + } ] } }, diff --git a/src/insertItem.ts b/src/insertItem.ts index 638bba757..5fabab574 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -116,8 +116,8 @@ export function getQuickPickItem(label: string): vscode.QuickPickItem { export function getInsertItemType(): QuickPickItem[] { let items: QuickPickItem[] = []; items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); - items.push(new QuickPickItem("Output", SortType.Outputs, "Inserts an output")); - items.push(new QuickPickItem("Parameter", SortType.Parameters, "Inserts a parameter")); + items.push(new QuickPickItem("Output", SortType.Outputs, "Insert an output")); + items.push(new QuickPickItem("Parameter", SortType.Parameters, "Insert a parameter")); items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); return items; From 7beaf7ac0cb76c77501bdcd25d99e341b05712b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 25 Apr 2020 17:32:34 +0200 Subject: [PATCH 50/61] Renamed SortType to TemplateSectionType --- extension.bundle.ts | 2 +- src/AzureRMTools.ts | 42 +++++++++++++------------- src/insertItem.ts | 31 +++++++++---------- src/sortTemplate.ts | 34 ++++++++++----------- test/functional/insertItem.test.ts | 48 +++++++++++++++--------------- 5 files changed, 79 insertions(+), 78 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index 711281b6a..ce638c267 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -59,7 +59,7 @@ export { ParameterValueDefinition } from "./src/parameterFiles/ParameterValueDef export { IReferenceSite, PositionContext } from "./src/PositionContext"; export { ReferenceList } from "./src/ReferenceList"; export { containsArmSchema, getPreferredSchema, isArmSchema } from './src/schemas'; -export { SortType } from "./src/sortTemplate"; +export { TemplateSectionType } from "./src/sortTemplate"; export * from "./src/survey"; export { TemplatePositionContext } from "./src/TemplatePositionContext"; export { ScopeContext, TemplateScope } from "./src/TemplateScope"; diff --git a/src/AzureRMTools.ts b/src/AzureRMTools.ts index 00cc59dcc..3162e8cd1 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -20,7 +20,7 @@ import { Histogram } from "./Histogram"; import * as Hover from './Hover'; import { DefinitionKind } from "./INamedDefinition"; import { IncorrectArgumentsCountIssue } from "./IncorrectArgumentsCountIssue"; -import { getInsertItemType, InsertItem } from "./insertItem"; +import { getItemTypeQuickPicks, InsertItem } from "./insertItem"; import * as Json from "./JSON"; import * as language from "./Language"; import { reloadSchemas } from "./languageclient/reloadSchemas"; @@ -34,7 +34,7 @@ import { ReferenceList } from "./ReferenceList"; import { resetGlobalState } from "./resetGlobalState"; import { getPreferredSchema } from "./schemas"; import { getFunctionParamUsage } from "./signatureFormatting"; -import { getQuickPickItems, sortTemplate, SortType } from "./sortTemplate"; +import { getQuickPickItems, sortTemplate, TemplateSectionType } from "./sortTemplate"; import { Stopwatch } from "./Stopwatch"; import { mightBeDeploymentParameters, mightBeDeploymentTemplate, templateDocumentSelector, templateOrParameterDocumentSelector } from "./supported"; import { survey } from "./survey"; @@ -131,27 +131,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) => { @@ -175,24 +175,24 @@ export class AzureRMTools { uri = vscode.window.activeTextEditor?.document.uri; } if (uri && editor) { - const sortType = await ext.ui.showQuickPick(getInsertItemType(), { placeHolder: 'What do you want to insert?' }); - await this.insertItem(sortType.value, uri, editor); + const sectionType = await ext.ui.showQuickPick(getItemTypeQuickPicks(), { placeHolder: 'What do you want to insert?' }); + await this.insertItem(sectionType.value, uri, editor); } }); registerCommand("azurerm-vscode-tools.insertParameter", async () => { - await this.insertItem(SortType.Parameters); + await this.insertItem(TemplateSectionType.Parameters); }); registerCommand("azurerm-vscode-tools.insertVariable", async () => { - await this.insertItem(SortType.Variables); + await this.insertItem(TemplateSectionType.Variables); }); registerCommand("azurerm-vscode-tools.insertOutput", async () => { - await this.insertItem(SortType.Outputs); + await this.insertItem(TemplateSectionType.Outputs); }); registerCommand("azurerm-vscode-tools.insertFunction", async () => { - await this.insertItem(SortType.Functions); + await this.insertItem(TemplateSectionType.Functions); }); registerCommand("azurerm-vscode-tools.insertResource", async () => { - await this.insertItem(SortType.Resources); + await this.insertItem(TemplateSectionType.Resources); }); registerCommand("azurerm-vscode-tools.resetGlobalState", resetGlobalState); registerCommand("azurerm-vscode-tools.codeAction.addAllMissingParameters", async (actionContext: IActionContext, source?: vscode.Uri) => { @@ -246,21 +246,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, sortType, editor); + await sortTemplate(deploymentTemplate, sectionType, editor); } } - private async insertItem(sortType: SortType, documentUri?: vscode.Uri, editor?: vscode.TextEditor): Promise { + private async insertItem(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 new InsertItem(ext.ui).insertItem(deploymentTemplate, sortType, editor); + await new InsertItem(ext.ui).insertItem(deploymentTemplate, sectionType, editor); } } diff --git a/src/insertItem.ts b/src/insertItem.ts index 5fabab574..817c6c6d3 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -10,7 +10,7 @@ import { IAzureUserInput } from "vscode-azureextensionui"; import { Json, templateKeys } from "../extension.bundle"; import { DeploymentTemplate } from "./DeploymentTemplate"; import { ext } from './extensionVariables'; -import { SortType } from "./sortTemplate"; +import { TemplateSectionType } from "./sortTemplate"; const insertCursorText = '[]'; @@ -113,13 +113,14 @@ export function getResourceSnippets(): vscode.QuickPickItem[] { export function getQuickPickItem(label: string): vscode.QuickPickItem { return { label: label }; } -export function getInsertItemType(): QuickPickItem[] { - let items: QuickPickItem[] = []; - items.push(new QuickPickItem("Function", SortType.Functions, "Insert a function")); - items.push(new QuickPickItem("Output", SortType.Outputs, "Insert an output")); - items.push(new QuickPickItem("Parameter", SortType.Parameters, "Insert a parameter")); - items.push(new QuickPickItem("Resource", SortType.Resources, "Insert a resource")); - items.push(new QuickPickItem("Variable", SortType.Variables, "Insert a variable")); + +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; } @@ -130,33 +131,33 @@ export class InsertItem { this.ui = ui; } - public async insertItem(template: DeploymentTemplate | undefined, sortType: SortType, textEditor: vscode.TextEditor): Promise { + public async insertItem(template: DeploymentTemplate | undefined, sectionType: TemplateSectionType, textEditor: vscode.TextEditor): Promise { if (!template) { return; } ext.outputChannel.appendLine("Insert item"); - switch (sortType) { - case SortType.Functions: + switch (sectionType) { + case TemplateSectionType.Functions: if (await this.insertFunction(template, textEditor)) { vscode.window.showInformationMessage("Please type the output of the function."); } break; - case SortType.Outputs: + case TemplateSectionType.Outputs: if (await this.insertOutput(template, textEditor)) { vscode.window.showInformationMessage("Please type the the value of the output."); } break; - case SortType.Parameters: + case TemplateSectionType.Parameters: if (await this.insertParameter(template, textEditor)) { vscode.window.showInformationMessage("Done inserting parameter."); } break; - case SortType.Resources: + case TemplateSectionType.Resources: if (await this.insertResource(template, textEditor)) { vscode.window.showInformationMessage("Press TAB to move between the tab stops."); } break; - case SortType.Variables: + case TemplateSectionType.Variables: if (await this.insertVariable(template, textEditor)) { vscode.window.showInformationMessage("Please type the the value of the variable."); } diff --git a/src/sortTemplate.ts b/src/sortTemplate.ts index ea9a3e6dd..aca0e4655 100644 --- a/src/sortTemplate.ts +++ b/src/sortTemplate.ts @@ -15,7 +15,7 @@ import { UserFunctionDefinition } from './UserFunctionDefinition'; import { UserFunctionNamespaceDefinition } from './UserFunctionNamespaceDefinition'; import { IVariableDefinition } from './VariableDefinition'; -export enum SortType { +export enum TemplateSectionType { Resources, Outputs, Parameters, @@ -29,10 +29,10 @@ 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,37 +41,37 @@ 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: diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 924bf5c90..c5830d870 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports import { commands, window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; -import { DeploymentTemplate, getResourceSnippets, InsertItem, SortType } from '../../extension.bundle'; +import { DeploymentTemplate, getResourceSnippets, InsertItem, TemplateSectionType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { @@ -94,8 +94,8 @@ suite("InsertItem", async (): Promise => { const totallyEmptyTemplate = `{}`; - async function doTestInsertItem(startTemplate: string, expectedTemplate: string, sortType: SortType, showInputBox: string[] = [], textToInsert: string = '', ignoreWhiteSpace: boolean = false): Promise { - await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, sortType, editor), showInputBox, textToInsert, ignoreWhiteSpace); + 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), showInputBox, textToInsert, ignoreWhiteSpace); } suite("Variables", async () => { @@ -122,17 +122,17 @@ suite("InsertItem", async (): Promise => { } }`; suite("Insert one variable", async () => { - await doTestInsertItem(emptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneVariableTemplate, TemplateSectionType.Variables, ["variable1"], 'resourceGroup()'); }); suite("Insert one more variable", async () => { - await doTestInsertItem(oneVariableTemplate, twoVariablesTemplate, SortType.Variables, ["variable2"], 'resourceGroup()'); + await doTestInsertItem(oneVariableTemplate, twoVariablesTemplate, TemplateSectionType.Variables, ["variable2"], 'resourceGroup()'); }); suite("Insert even one more variable", async () => { - await doTestInsertItem(twoVariablesTemplate, threeVariablesTemplate, SortType.Variables, ["variable3"], 'resourceGroup()'); + await doTestInsertItem(twoVariablesTemplate, threeVariablesTemplate, TemplateSectionType.Variables, ["variable3"], 'resourceGroup()'); }); suite("Insert one variable in totally empty template", async () => { - await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, SortType.Variables, ["variable1"], 'resourceGroup()'); + await doTestInsertItem(totallyEmptyTemplate, oneVariableTemplate, TemplateSectionType.Variables, ["variable1"], 'resourceGroup()'); }); }); @@ -175,13 +175,13 @@ suite("InsertItem", async (): Promise => { }`; suite("Insert one resource (KeyVault Secret) into totally empty template", async () => { - await doTestInsertItem(totallyEmptyTemplate, oneResourceTemplate, SortType.Resources, ["KeyVault Secret"], '', true); + await doTestInsertItem(totallyEmptyTemplate, oneResourceTemplate, TemplateSectionType.Resources, ["KeyVault Secret"], '', true); }); suite("Insert one resource (KeyVault Secret)", async () => { - await doTestInsertItem(emptyTemplate, oneResourceTemplate, SortType.Resources, ["KeyVault Secret"], '', true); + await doTestInsertItem(emptyTemplate, oneResourceTemplate, TemplateSectionType.Resources, ["KeyVault Secret"], '', true); }); suite("Insert one more resource (Application Security Group)", async () => { - await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, SortType.Resources, ["Application Security Group"], '', true); + await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, TemplateSectionType.Resources, ["Application Security Group"], '', true); }); suite("Resource snippets", async () => { @@ -307,22 +307,22 @@ suite("InsertItem", async (): Promise => { ] }`; suite("Insert function", async () => { - await doTestInsertItem(emptyTemplate, oneFunctionTemplate, SortType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); + await doTestInsertItem(emptyTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); }); suite("Insert one more function", async () => { - await doTestInsertItem(oneFunctionTemplate, twoFunctionsTemplate, SortType.Functions, ["function2", "String", ""], "resourceGroup()"); + await doTestInsertItem(oneFunctionTemplate, twoFunctionsTemplate, TemplateSectionType.Functions, ["function2", "String", ""], "resourceGroup()"); }); suite("Insert one function in totally empty template", async () => { - await doTestInsertItem(totallyEmptyTemplate, oneFunctionTemplate, SortType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); + await doTestInsertItem(totallyEmptyTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["ns", "function1", "String", "parameter1", "String", ""], "resourceGroup()"); }); suite("Insert function in namespace", async () => { - await doTestInsertItem(namespaceTemplate, oneFunctionTemplate, SortType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); + await doTestInsertItem(namespaceTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); }); suite("Insert function in members", async () => { - await doTestInsertItem(membersTemplate, oneFunctionTemplate, SortType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); + await doTestInsertItem(membersTemplate, oneFunctionTemplate, TemplateSectionType.Functions, ["function1", "String", "parameter1", "String", ""], "resourceGroup()"); }); suite("Insert even one more function", async () => { - await doTestInsertItem(twoFunctionsTemplate, threeFunctionsTemplate, SortType.Functions, ["function3", "Secure string", "parameter1", "String", "parameter2", "Bool", ""], "resourceGroup()"); + await doTestInsertItem(twoFunctionsTemplate, threeFunctionsTemplate, TemplateSectionType.Functions, ["function3", "Secure string", "parameter1", "String", "parameter2", "Bool", ""], "resourceGroup()"); }); }); @@ -377,16 +377,16 @@ suite("InsertItem", async (): Promise => { } }`; suite("Insert one parameter", async () => { - await doTestInsertItem(emptyTemplate, oneParameterTemplate, SortType.Parameters, ["parameter1", "String", "default", "description"]); + await doTestInsertItem(emptyTemplate, oneParameterTemplate, TemplateSectionType.Parameters, ["parameter1", "String", "default", "description"]); }); suite("Insert one more parameter", async () => { - await doTestInsertItem(oneParameterTemplate, twoParametersTemplate, SortType.Parameters, ["parameter2", "String", "", ""]); + await doTestInsertItem(oneParameterTemplate, twoParametersTemplate, TemplateSectionType.Parameters, ["parameter2", "String", "", ""]); }); suite("Insert even one more parameter", async () => { - await doTestInsertItem(twoParametersTemplate, threeParametersTemplate, SortType.Parameters, ["parameter3", "Secure string", "", "description3"]); + await doTestInsertItem(twoParametersTemplate, threeParametersTemplate, TemplateSectionType.Parameters, ["parameter3", "Secure string", "", "description3"]); }); suite("Insert one output in totally empty template", async () => { - await doTestInsertItem(totallyEmptyTemplate, oneParameterTemplate, SortType.Parameters, ["parameter1", "String", "default", "description"]); + await doTestInsertItem(totallyEmptyTemplate, oneParameterTemplate, TemplateSectionType.Parameters, ["parameter1", "String", "default", "description"]); }); }); @@ -432,16 +432,16 @@ suite("InsertItem", async (): Promise => { } }`; suite("Insert one output", async () => { - await doTestInsertItem(emptyTemplate, oneOutputTemplate, SortType.Outputs, ["output1", "String"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneOutputTemplate, TemplateSectionType.Outputs, ["output1", "String"], 'resourceGroup()'); }); suite("Insert one more output", async () => { - await doTestInsertItem(oneOutputTemplate, twoOutputsTemplate, SortType.Outputs, ["output2", "String"], 'resourceGroup()'); + await doTestInsertItem(oneOutputTemplate, twoOutputsTemplate, TemplateSectionType.Outputs, ["output2", "String"], 'resourceGroup()'); }); suite("Insert even one more output", async () => { - await doTestInsertItem(twoOutputsTemplate, threeOutputsTemplate, SortType.Outputs, ["output3", "Secure string"], 'resourceGroup()'); + await doTestInsertItem(twoOutputsTemplate, threeOutputsTemplate, TemplateSectionType.Outputs, ["output3", "Secure string"], 'resourceGroup()'); }); suite("Insert one output in totally empty template", async () => { - await doTestInsertItem(emptyTemplate, oneOutputTemplate, SortType.Outputs, ["output1", "String"], 'resourceGroup()'); + await doTestInsertItem(emptyTemplate, oneOutputTemplate, TemplateSectionType.Outputs, ["output1", "String"], 'resourceGroup()'); }); }); }); From 6af6f9582b57f4a8137539d858bdae95bbc6f5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 25 Apr 2020 18:53:51 +0200 Subject: [PATCH 51/61] Refactored getContextValue in Treeview --- src/Treeview.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Treeview.ts b/src/Treeview.ts index 31903fe32..26440ea8e 100644 --- a/src/Treeview.ts +++ b/src/Treeview.ts @@ -297,16 +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; - } - } else { - const rootNode = this.tree && this.tree.getValueAtCharacterIndex(elementInfo.root.key.start, Contains.strict); - if (rootNode instanceof Json.StringValue) { - return rootNode.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; } From e2700579553b2daf21caefaa9b1083d633d025aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 25 Apr 2020 19:05:35 +0200 Subject: [PATCH 52/61] Fixed failing unit tests --- test/functional/insertItem.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index c5830d870..073e1cc2c 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -168,8 +168,10 @@ suite("InsertItem", async (): Promise => { "type": "Microsoft.Network/applicationSecurityGroups", "apiVersion": "2019-11-01", "location": "[resourceGroup().location]", - "tags": {}, - "properties": {} + "tags": { + }, + "properties": { + } } ] }`; From fb20dc2cfbc21bc973e46b4f497254f46908616c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 26 Apr 2020 01:18:54 +0200 Subject: [PATCH 53/61] Changed so that insertItem.getResourceSnippets reads available resource snippets from assets/armsnippets.jsonc --- extension.bundle.ts | 2 +- src/insertItem.ts | 104 ++++++++++------------------- test/functional/insertItem.test.ts | 10 +-- 3 files changed, 37 insertions(+), 79 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index 730127f3c..ed5585b29 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -46,7 +46,7 @@ export { HoverInfo } from "./src/Hover"; export { httpGet } from './src/httpGet'; export { DefinitionKind, INamedDefinition } from "./src/INamedDefinition"; export { IncorrectArgumentsCountIssue } from "./src/IncorrectArgumentsCountIssue"; -export { getResourceSnippets, InsertItem } from "./src/insertItem"; +export { InsertItem } from "./src/insertItem"; export { IParameterDefinition } from "./src/IParameterDefinition"; export * from "./src/Language"; export { LanguageServerState } from "./src/languageclient/startArmLanguageServer"; diff --git a/src/insertItem.ts b/src/insertItem.ts index 817c6c6d3..810109aaf 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +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 { 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 "./sortTemplate"; @@ -38,76 +41,39 @@ export function getItemType(): QuickPickItem[] { return items; } -export function getResourceSnippets(): vscode.QuickPickItem[] { +function getResourceSnippets(): vscode.QuickPickItem[] { let items: vscode.QuickPickItem[] = []; - items.push(getQuickPickItem("Nested Deployment")); - items.push(getQuickPickItem("App Service Plan (Server Farm)")); - items.push(getQuickPickItem("Application Insights for Web Apps")); - items.push(getQuickPickItem("Application Security Group")); - items.push(getQuickPickItem("Automation Account")); - items.push(getQuickPickItem("Automation Certificate")); - items.push(getQuickPickItem("Automation Credential")); - items.push(getQuickPickItem("Automation Job Schedule")); - items.push(getQuickPickItem("Automation Runbook")); - items.push(getQuickPickItem("Automation Schedule")); - items.push(getQuickPickItem("Automation Variable")); - items.push(getQuickPickItem("Automation Module")); - items.push(getQuickPickItem("Availability Set")); - items.push(getQuickPickItem("Azure Firewall")); - items.push(getQuickPickItem("Container Group")); - items.push(getQuickPickItem("Container Registry")); - items.push(getQuickPickItem("Cosmos DB Database Account")); - items.push(getQuickPickItem("Cosmos DB SQL Database")); - items.push(getQuickPickItem("Cosmos DB Mongo Database")); - items.push(getQuickPickItem("Cosmos DB Gremlin Database")); - items.push(getQuickPickItem("Cosmos DB Cassandra Namespace")); - items.push(getQuickPickItem("Cosmos DB Cassandra Table")); - items.push(getQuickPickItem("Cosmos DB SQL Container")); - items.push(getQuickPickItem("Cosmos DB Gremlin Graph")); - items.push(getQuickPickItem("Cosmos DB Table Storage Table")); - items.push(getQuickPickItem("Data Lake Store Account")); - items.push(getQuickPickItem("DNS Record")); - items.push(getQuickPickItem("DNS Zone")); - items.push(getQuickPickItem("Function")); - items.push(getQuickPickItem("KeyVault")); - items.push(getQuickPickItem("KeyVault Secret")); - items.push(getQuickPickItem("Kubernetes Service Cluster")); - items.push(getQuickPickItem("Linux VM Custom Script")); - items.push(getQuickPickItem("Load Balancer External")); - items.push(getQuickPickItem("Load Balancer Internal")); - items.push(getQuickPickItem("Log Analytics Solution")); - items.push(getQuickPickItem("Log Analytics Workspace")); - items.push(getQuickPickItem("Logic App")); - items.push(getQuickPickItem("Logic App Connector")); - items.push(getQuickPickItem("Managed Identity (User Assigned)")); - items.push(getQuickPickItem("Media Services")); - items.push(getQuickPickItem("MySQL Database")); - items.push(getQuickPickItem("Network Interface")); - items.push(getQuickPickItem("Network Security Group")); - items.push(getQuickPickItem("Network Security Group Rule")); - items.push(getQuickPickItem("Public IP Address")); - items.push(getQuickPickItem("Public IP Prefix")); - items.push(getQuickPickItem("Recovery Service Vault")); - items.push(getQuickPickItem("Redis Cache")); - items.push(getQuickPickItem("Route Table")); - items.push(getQuickPickItem("Route Table Route")); - items.push(getQuickPickItem("SQL Database")); - items.push(getQuickPickItem("SQL Database Import")); - items.push(getQuickPickItem("SQL Server")); - items.push(getQuickPickItem("Storage Account")); - items.push(getQuickPickItem("Traffic Manager Profile")); - items.push(getQuickPickItem("Ubuntu Virtual Machine")); - items.push(getQuickPickItem("Virtual Network")); - items.push(getQuickPickItem("VPN Local Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Gateway")); - items.push(getQuickPickItem("VPN Virtual Network Connection")); - items.push(getQuickPickItem("Web App")); - items.push(getQuickPickItem("Web Deploy for Web App")); - items.push(getQuickPickItem("Windows Virtual Machine")); - items.push(getQuickPickItem("Windows VM Custom Script")); - items.push(getQuickPickItem("Windows VM Diagnostics Extension")); - items.push(getQuickPickItem("Windows VM DSC PowerShell Script")); - return items; + 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 { diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 073e1cc2c..01ed5f584 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports import { commands, window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; -import { DeploymentTemplate, getResourceSnippets, InsertItem, TemplateSectionType } from '../../extension.bundle'; +import { DeploymentTemplate, InsertItem, TemplateSectionType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; suite("InsertItem", async (): Promise => { @@ -185,14 +185,6 @@ suite("InsertItem", async (): Promise => { suite("Insert one more resource (Application Security Group)", async () => { await doTestInsertItem(oneResourceTemplate, twoResourcesTemplate, TemplateSectionType.Resources, ["Application Security Group"], '', true); }); - - suite("Resource snippets", async () => { - for (const snippet of getResourceSnippets()) { - test(snippet.label, async () => { - await testResourceSnippet(snippet.label); - }); - } - }); }); suite("Functions", async () => { From 4e12e2dbada3f988c6526388c5bd4de6b25992f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sun, 26 Apr 2020 02:27:04 +0200 Subject: [PATCH 54/61] Removed unused function testResourceSnippet in insertItem.test.ts --- test/functional/insertItem.test.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/functional/insertItem.test.ts b/test/functional/insertItem.test.ts index 01ed5f584..e92923811 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -11,7 +11,7 @@ import * as assert from 'assert'; import * as fse from 'fs-extra'; import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports -import { commands, window, workspace } from "vscode"; +import { window, workspace } from "vscode"; import { IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, TemplateSectionType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; @@ -81,16 +81,6 @@ suite("InsertItem", async (): Promise => { assertTemplate(docTextAfterInsertion, expected, textEditor, ignoreWhiteSpace); } - async function testResourceSnippet(resourceSnippet: string): Promise { - const tempPath = getTempFilePath(`insertItem`, '.azrm'); - fse.writeFileSync(tempPath, ''); - let document = await workspace.openTextDocument(tempPath); - await window.showTextDocument(document); - let timeout = setTimeout(() => assert.fail(`Invalid resource snippet: ${resourceSnippet}`), 1000); - await commands.executeCommand('editor.action.insertSnippet', { name: resourceSnippet }); - clearTimeout(timeout); - } - const totallyEmptyTemplate = `{}`; From ffa0ad7d549845a4995693a4a094a4ae8208331a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 1 May 2020 16:11:54 +0200 Subject: [PATCH 55/61] Changes to package.json based on feedback on PR --- package.json | 74 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0e632c60b..ecf554a35 100644 --- a/package.json +++ b/package.json @@ -403,52 +403,102 @@ { "$comment": "============= Treeview commands =============", "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": "view == azurerm-vscode-tools.template-outline && viewItem == outputs", + "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.sortParameters", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters", + "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.sortResources", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources", + "group": "arm-template" + }, + { + "command": "azurerm-vscode-tools.sortVariables", + "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": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs", "group": "inline" }, { "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": "inline" }, { "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": "inline" }, { "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": "inline" }, { "command": "azurerm-vscode-tools.insertParameter", - "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == parameters", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == parameters", "group": "inline" }, { "command": "azurerm-vscode-tools.insertVariable", - "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == variables", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == variables", "group": "inline" }, { "command": "azurerm-vscode-tools.insertOutput", - "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == outputs", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == outputs", "group": "inline" }, { "command": "azurerm-vscode-tools.insertFunction", - "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == functions", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == functions", "group": "inline" }, { "command": "azurerm-vscode-tools.insertResource", - "when": "azurerm-vscode-tools.template-outline.active == true && viewItem == resources", + "when": "view == azurerm-vscode-tools.template-outline && viewItem == resources", "group": "inline" } ], @@ -497,12 +547,12 @@ "view/title": [ { "command": "azurerm-vscode-tools.insertItem", - "when": "azurerm-vscode-tools.template-outline.active == true", + "when": "view == azurerm-vscode-tools.template-outline", "group": "navigation@1" }, { "command": "azurerm-vscode-tools.sortTemplate", - "when": "azurerm-vscode-tools.template-outline.active == true", + "when": "view == azurerm-vscode-tools.template-outline", "group": "navigation@2" } ] From 0e082bced17dc4335ac83f529768b66f63b7fb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Fri, 1 May 2020 16:20:28 +0200 Subject: [PATCH 56/61] Moved TemplateSectionType to separate file --- src/TemplateSectionType.ts | 15 +++++++++++++++ src/sortTemplate.ts | 9 --------- 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 src/TemplateSectionType.ts 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/sortTemplate.ts b/src/sortTemplate.ts index aca0e4655..63b56b610 100644 --- a/src/sortTemplate.ts +++ b/src/sortTemplate.ts @@ -15,15 +15,6 @@ import { UserFunctionDefinition } from './UserFunctionDefinition'; import { UserFunctionNamespaceDefinition } from './UserFunctionNamespaceDefinition'; import { IVariableDefinition } from './VariableDefinition'; -export enum TemplateSectionType { - Resources, - Outputs, - Parameters, - Variables, - Functions, - TopLevel -} - // A map of [token starting index] to [span of all comments before that token] type CommentsMap = Map; From d40153ce7836914c7a41d75aab68f3d9cb95d563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 2 May 2020 00:27:15 +0200 Subject: [PATCH 57/61] Throw error instead of show error message in insertItem --- extension.bundle.ts | 2 +- src/AzureRMTools.ts | 31 ++++---- src/insertItem.ts | 110 +++++++++++++---------------- src/sortTemplate.ts | 5 +- test/functional/insertItem.test.ts | 21 +++++- 5 files changed, 89 insertions(+), 80 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index ed5585b29..89edd9661 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -60,10 +60,10 @@ export { ParameterValueDefinition } from "./src/parameterFiles/ParameterValueDef export { IReferenceSite, PositionContext } from "./src/PositionContext"; export { ReferenceList } from "./src/ReferenceList"; export { containsArmSchema, getPreferredSchema, isArmSchema } from './src/schemas'; -export { TemplateSectionType } from "./src/sortTemplate"; 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/src/AzureRMTools.ts b/src/AzureRMTools.ts index 07ca69f33..dfb06df2a 100644 --- a/src/AzureRMTools.ts +++ b/src/AzureRMTools.ts @@ -34,11 +34,12 @@ import { ReferenceList } from "./ReferenceList"; import { resetGlobalState } from "./resetGlobalState"; import { getPreferredSchema } from "./schemas"; import { getFunctionParamUsage } from "./signatureFormatting"; -import { getQuickPickItems, sortTemplate, TemplateSectionType } 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"; @@ -169,7 +170,7 @@ export class AzureRMTools { source = source ?? vscode.window.activeTextEditor?.document.uri; await openTemplateFile(this._mapping, source, undefined); }); - registerCommand("azurerm-vscode-tools.insertItem", async (_context: IActionContext, uri?: vscode.Uri, editor?: vscode.TextEditor) => { + 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 @@ -178,23 +179,23 @@ export class AzureRMTools { } if (uri && editor) { const sectionType = await ext.ui.showQuickPick(getItemTypeQuickPicks(), { placeHolder: 'What do you want to insert?' }); - await this.insertItem(sectionType.value, uri, editor); + await this.insertItem(sectionType.value, actionContext, uri, editor); } }); - registerCommand("azurerm-vscode-tools.insertParameter", async () => { - await this.insertItem(TemplateSectionType.Parameters); + registerCommand("azurerm-vscode-tools.insertParameter", async (actionContext: IActionContext) => { + await this.insertItem(TemplateSectionType.Parameters, actionContext); }); - registerCommand("azurerm-vscode-tools.insertVariable", async () => { - await this.insertItem(TemplateSectionType.Variables); + registerCommand("azurerm-vscode-tools.insertVariable", async (actionContext: IActionContext) => { + await this.insertItem(TemplateSectionType.Variables, actionContext); }); - registerCommand("azurerm-vscode-tools.insertOutput", async () => { - await this.insertItem(TemplateSectionType.Outputs); + registerCommand("azurerm-vscode-tools.insertOutput", async (actionContext: IActionContext) => { + await this.insertItem(TemplateSectionType.Outputs, actionContext); }); - registerCommand("azurerm-vscode-tools.insertFunction", async () => { - await this.insertItem(TemplateSectionType.Functions); + registerCommand("azurerm-vscode-tools.insertFunction", async (actionContext: IActionContext) => { + await this.insertItem(TemplateSectionType.Functions, actionContext); }); - registerCommand("azurerm-vscode-tools.insertResource", async () => { - await this.insertItem(TemplateSectionType.Resources); + 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) => { @@ -257,12 +258,12 @@ export class AzureRMTools { } } - private async insertItem(sectionType: TemplateSectionType, documentUri?: vscode.Uri, editor?: vscode.TextEditor): Promise { + 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 new InsertItem(ext.ui).insertItem(deploymentTemplate, sectionType, editor); + await new InsertItem(ext.ui).insertItem(deploymentTemplate, sectionType, editor, context); } } diff --git a/src/insertItem.ts b/src/insertItem.ts index 810109aaf..c5acaaa36 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -3,17 +3,19 @@ * 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 { IAzureUserInput } from "vscode-azureextensionui"; +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 "./sortTemplate"; +import { TemplateSectionType } from "./TemplateSectionType"; +import { assertNever } from './util/assertNever'; const insertCursorText = '[]'; @@ -97,88 +99,78 @@ export class InsertItem { this.ui = ui; } - public async insertItem(template: DeploymentTemplate | undefined, sectionType: TemplateSectionType, textEditor: vscode.TextEditor): Promise { + 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: - if (await this.insertFunction(template, textEditor)) { - vscode.window.showInformationMessage("Please type the output of the function."); - } + await this.insertFunction(template, textEditor, context); + vscode.window.showInformationMessage("Please type the output of the function."); break; case TemplateSectionType.Outputs: - if (await this.insertOutput(template, textEditor)) { - vscode.window.showInformationMessage("Please type the the value of the output."); - } + await this.insertOutput(template, textEditor, context); + vscode.window.showInformationMessage("Please type the the value of the output."); break; case TemplateSectionType.Parameters: - if (await this.insertParameter(template, textEditor)) { - vscode.window.showInformationMessage("Done inserting parameter."); - } + await this.insertParameter(template, textEditor, context); + vscode.window.showInformationMessage("Done inserting parameter."); break; case TemplateSectionType.Resources: - if (await this.insertResource(template, textEditor)) { - vscode.window.showInformationMessage("Press TAB to move between the tab stops."); - } + await this.insertResource(template, textEditor, context); + vscode.window.showInformationMessage("Press TAB to move between the tab stops."); break; case TemplateSectionType.Variables: - if (await this.insertVariable(template, textEditor)) { - vscode.window.showInformationMessage("Please type the the value of the variable."); - } + 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: - vscode.window.showWarningMessage("Unknown insert item type!"); - return; + assertNever(sectionType); } } private getTemplateObjectPart(template: DeploymentTemplate, templatePart: string): Json.ObjectValue | undefined { - let part = this.getTemplatePart(template, templatePart); - return Json.asObjectValue(part); + return this.getTemplatePart(template, templatePart)?.asObjectValue; } private getTemplateArrayPart(template: DeploymentTemplate, templatePart: string): Json.ArrayValue | undefined { - let part = this.getTemplatePart(template, templatePart); - return Json.asArrayValue(part); + return this.getTemplatePart(template, templatePart)?.asArrayValue; } private getTemplatePart(template: DeploymentTemplate, templatePart: string): Json.Value | undefined { - let rootValue = template.topLevelValue; - if (!rootValue) { - return undefined; - } - return rootValue.getPropertyValue(templatePart); + return template.topLevelValue?.getPropertyValue(templatePart); } - private async insertParameter(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + 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 !== '') { + if (defaultValue) { parameter.defaultValue = defaultValue; } let description = await this.ui.showInputBox({ prompt: "Description? Leave empty for no description.", }); - if (description !== '') { + if (description) { parameter.metadata = { description: description }; } - return await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name); + await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name, context); } // tslint:disable-next-line:no-any - private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string): Promise { + private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string, context: IActionContext): Promise { let templatePart = this.getTemplateObjectPart(template, part); if (!templatePart) { let topLevel = template.topLevelValue; if (!topLevel) { - vscode.window.showErrorMessage('Invalid ARM template!'); - return false; + context.errorHandling.suppressReportIssue = true; + throw new Error("Invalid ARM template!"); } // tslint:disable-next-line:no-any let subPart: any = {}; @@ -188,7 +180,6 @@ export class InsertItem { } else { await this.insertInObjectInternal(templatePart, textEditor, data, name); } - return true; } // tslint:disable-next-line:no-any @@ -203,34 +194,32 @@ export class InsertItem { return await this.insertText(textEditor, index, `${startText}${indentedText}${endText}`); } - private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertVariable(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise { let name = await this.ui.showInputBox({ prompt: "Name of variable?" }); - return await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name); + await this.insertInObject(template, textEditor, templateKeys.variables, insertCursorText, name, context); } - private async insertOutput(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + 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, '') }; - return await this.insertInObject(template, textEditor, templateKeys.outputs, output, name); + await this.insertInObject(template, textEditor, templateKeys.outputs, output, name, context); } - private async insertFunctionAsTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor): Promise { + private async insertFunctionAsTopLevel(topLevel: Json.ObjectValue | undefined, textEditor: vscode.TextEditor, context: IActionContext): Promise { if (!topLevel) { - vscode.window.showErrorMessage('Invalid ARM template!'); - return false; + context.errorHandling.suppressReportIssue = true; + throw new Error("Invalid ARM template!"); } let functions = [await this.getFunctionNamespace()]; await this.insertInObjectInternal(topLevel, textEditor, functions, "functions", 1); - return true; } - private async insertFunctionAsNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { + private async insertFunctionAsNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { let namespace = await this.getFunctionNamespace(); await this.insertInArray(functions, textEditor, namespace); - return true; } private async insertFunctionAsMembers(namespace: Json.ObjectValue, textEditor: vscode.TextEditor): Promise { @@ -249,43 +238,45 @@ export class InsertItem { await this.insertInObjectInternal(members, textEditor, functionDef, functionName, 4); } - private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + 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 - return await this.insertFunctionAsTopLevel(template.topLevelValue, textEditor); + await this.insertFunctionAsTopLevel(template.topLevelValue, textEditor, context); + return; } if (functions.length === 0) { - return await this.insertFunctionAsNamespace(functions, textEditor); + await this.insertFunctionAsNamespace(functions, textEditor); + return; } let namespace = Json.asObjectValue(functions.elements[0]); if (!namespace) { - vscode.window.showErrorMessage('The first namespace in functions is not an object!'); - return false; + 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 true; + return; } let membersObject = Json.asObjectValue(members); if (!membersObject) { - vscode.window.showErrorMessage('The first namespace in functions does not have members as an object!'); - return false; + 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 true; + return; } - private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor): Promise { + private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); let pos: vscode.Position; let index: number; let text = "\r\n\t\t\r\n\t"; if (!resources) { if (!template.topLevelValue) { - vscode.window.showErrorMessage("Invalid ARM template!"); - return false; + context.errorHandling.suppressReportIssue = true; + throw new Error("Invalid ARM template!"); } // tslint:disable-next-line:no-any let subPart: any = []; @@ -310,7 +301,6 @@ export class InsertItem { textEditor.selection = newSelection; await commands.executeCommand('editor.action.insertSnippet', { name: resource.label }); textEditor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.Default); - return true; } private async getFunction(): Promise { diff --git a/src/sortTemplate.ts b/src/sortTemplate.ts index 63b56b610..63313b6d3 100644 --- a/src/sortTemplate.ts +++ b/src/sortTemplate.ts @@ -11,8 +11,10 @@ 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'; // A map of [token starting index] to [span of all comments before that token] @@ -66,8 +68,7 @@ export async function sortTemplate(template: DeploymentTemplate | undefined, sec 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 index e92923811..d84a54990 100644 --- a/test/functional/insertItem.test.ts +++ b/test/functional/insertItem.test.ts @@ -12,7 +12,7 @@ import * as fse from 'fs-extra'; import * as vscode from "vscode"; // tslint:disable-next-line:no-duplicate-imports import { window, workspace } from "vscode"; -import { IAzureUserInput } from 'vscode-azureextensionui'; +import { IActionContext, IAzureUserInput } from 'vscode-azureextensionui'; import { DeploymentTemplate, InsertItem, TemplateSectionType } from '../../extension.bundle'; import { getTempFilePath } from "../support/getTempFilePath"; @@ -85,7 +85,7 @@ suite("InsertItem", async (): Promise => { `{}`; 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), showInputBox, textToInsert, ignoreWhiteSpace); + await testInsertItem(startTemplate, expectedTemplate, async (insertItem, template, editor) => await insertItem.insertItem(template, sectionType, editor, getActionContext()), showInputBox, textToInsert, ignoreWhiteSpace); } suite("Variables", async () => { @@ -454,3 +454,20 @@ class MockUserInput implements IAzureUserInput { 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 + } + }; +} From 3fad09fe8ddccd9ef55ff87807b868f7a09671a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 2 May 2020 00:45:16 +0200 Subject: [PATCH 58/61] Fixed indentation function --- src/insertItem.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index c5acaaa36..84e65f7c4 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -392,14 +392,14 @@ export class InsertItem { /** * Indents the given string * @param str The string to be indented. - * @param numOfIndents The amount of indentations to place at the + * @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, numOfIndents: number): string { + private indent(str: string, numOfTabs: number): string { // tslint:disable-next-line:prefer-array-literal - str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t')); + str = str.replace(/^(?=.)/gm, '\t'.repeat(numOfTabs)); return str; } } From 68f19a6848c13a013a81290ab1d2cdd63da97225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 2 May 2020 01:11:04 +0200 Subject: [PATCH 59/61] Made changes based on feedback on PR --- src/insertItem.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index 84e65f7c4..d42aa1c07 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -163,8 +163,7 @@ export class InsertItem { await this.insertInObject(template, textEditor, templateKeys.parameters, parameter, name, context); } - // tslint:disable-next-line:no-any - private async insertInObject(template: DeploymentTemplate, textEditor: vscode.TextEditor, part: string, data: any, name: string, context: IActionContext): Promise { + 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; @@ -172,21 +171,20 @@ export class InsertItem { context.errorHandling.suppressReportIssue = true; throw new Error("Invalid ARM template!"); } - // tslint:disable-next-line:no-any - let subPart: any = {}; - // tslint:disable-next-line:no-unsafe-any + let subPart: Data = {}; subPart[name] = data; - await this.insertInObjectInternal(topLevel, textEditor, subPart, part, 1); + await this.insertInObjectHelper(topLevel, textEditor, subPart, part, 1); } else { - await this.insertInObjectInternal(templatePart, textEditor, data, name); + await this.insertInObjectHelper(templatePart, textEditor, data, name); } } // tslint:disable-next-line:no-any - private async insertInObjectInternal(templatePart: Json.ObjectValue, textEditor: vscode.TextEditor, data: any, name: string, indentLevel: number = 2): Promise { + 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 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}"`; @@ -214,7 +212,7 @@ export class InsertItem { throw new Error("Invalid ARM template!"); } let functions = [await this.getFunctionNamespace()]; - await this.insertInObjectInternal(topLevel, textEditor, functions, "functions", 1); + await this.insertInObjectHelper(topLevel, textEditor, functions, "functions", 1); } private async insertFunctionAsNamespace(functions: Json.ArrayValue, textEditor: vscode.TextEditor): Promise { @@ -229,13 +227,13 @@ export class InsertItem { let members: any = {}; // tslint:disable-next-line:no-unsafe-any members[functionName] = functionDef; - await this.insertInObjectInternal(namespace, textEditor, members, 'members', 3); + 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.insertInObjectInternal(members, textEditor, functionDef, functionName, 4); + await this.insertInObjectHelper(members, textEditor, functionDef, functionName, 4); } private async insertFunction(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise { @@ -280,7 +278,7 @@ export class InsertItem { } // tslint:disable-next-line:no-any let subPart: any = []; - index = await this.insertInObjectInternal(template.topLevelValue, textEditor, subPart, "resources", 1); + index = await this.insertInObjectHelper(template.topLevelValue, textEditor, subPart, "resources", 1); pos = textEditor.selection.active; } else { index = resources.span.endIndex; @@ -408,25 +406,25 @@ interface ParameterMetaData { description: string; } -interface Parameter { +interface Parameter extends Data { // tslint:disable-next-line:no-reserved-keywords type: string; defaultValue?: string; metadata?: ParameterMetaData; } -interface Output { +interface Output extends Data { // tslint:disable-next-line:no-reserved-keywords type: string; value: string; } -interface Function { +interface Function extends Data { parameters: Parameter[]; output: Output; } -interface FunctionParameter { +interface FunctionParameter extends Data { name: string; // tslint:disable-next-line:no-reserved-keywords type: string; @@ -437,3 +435,5 @@ interface FunctionNameSpace { // tslint:disable-next-line:no-any members: any[]; } + +type Data = { [key: string]: unknown }; From 26c73ce6185040d9773c4da7e4ed642a13692492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 2 May 2020 02:14:07 +0200 Subject: [PATCH 60/61] Minor changes based on feedback on PR --- src/insertItem.ts | 50 +++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index d42aa1c07..a59a67bc3 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -179,6 +179,15 @@ export class InsertItem { } } + /** + * 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; @@ -270,7 +279,7 @@ export class InsertItem { let resources = this.getTemplateArrayPart(template, templateKeys.resources); let pos: vscode.Position; let index: number; - let text = "\r\n\t\t\r\n\t"; + let prepend = "\r\n\t\t\r\n\t"; if (!resources) { if (!template.topLevelValue) { context.errorHandling.suppressReportIssue = true; @@ -278,27 +287,30 @@ export class InsertItem { } // tslint:disable-next-line:no-any let subPart: any = []; - index = await this.insertInObjectHelper(template.topLevelValue, textEditor, subPart, "resources", 1); + index = await this.insertInObjectHelper(template.topLevelValue, textEditor, subPart, templateKeys.resources, 1); pos = textEditor.selection.active; } else { index = resources.span.endIndex; if (resources.elements.length > 0) { let lastIndex = resources.elements.length - 1; index = resources.elements[lastIndex].span.afterEndIndex; - text = `,\r\n\t\t`; + 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, text, false); - let range = new vscode.Range(textEditor.document.positionAt(index), textEditor.document.positionAt(index + this.formatText(text, textEditor).length)); - let insertedText = textEditor.document.getText(range); - let lookFor = this.formatText('\t\t', textEditor); - let cursorPos = insertedText.indexOf(lookFor); - pos = textEditor.document.positionAt(index + cursorPos + lookFor.length); - let newSelection = new vscode.Selection(pos, pos); - textEditor.selection = newSelection; + 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(pos, pos), vscode.TextEditorRevealType.Default); + 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 { @@ -336,7 +348,7 @@ export class InsertItem { 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`, true); + await this.insertText(textEditor, index, `${indentedText}\t`); } private async getFunctionNamespace(): Promise { @@ -359,7 +371,14 @@ export class InsertItem { return members; } - private async insertText(textEditor: vscode.TextEditor, index: number, text: string, setCursor: boolean = false): Promise { + /** + * 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)); @@ -381,9 +400,6 @@ export class InsertItem { } else { text = text.replace(/ {4}/g, '\t'); } - if (textEditor.document.eol === vscode.EndOfLine.LF) { - text = text.replace(/\r\n/g, '\n'); - } return text; } From 4defcea68ca7b1f5ac823aba99bbe2bf0d3c04ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hedstr=C3=B6m=2C=20Nils?= Date: Sat, 2 May 2020 02:46:35 +0200 Subject: [PATCH 61/61] Fixed compilation error --- src/insertItem.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/insertItem.ts b/src/insertItem.ts index a59a67bc3..62dc09fd2 100644 --- a/src/insertItem.ts +++ b/src/insertItem.ts @@ -277,7 +277,6 @@ export class InsertItem { private async insertResource(template: DeploymentTemplate, textEditor: vscode.TextEditor, context: IActionContext): Promise { let resources = this.getTemplateArrayPart(template, templateKeys.resources); - let pos: vscode.Position; let index: number; let prepend = "\r\n\t\t\r\n\t"; if (!resources) { @@ -288,7 +287,6 @@ export class InsertItem { // tslint:disable-next-line:no-any let subPart: any = []; index = await this.insertInObjectHelper(template.topLevelValue, textEditor, subPart, templateKeys.resources, 1); - pos = textEditor.selection.active; } else { index = resources.span.endIndex; if (resources.elements.length > 0) {