diff --git a/src/documents/templates/getDependsOnCompletions.ts b/src/documents/templates/getDependsOnCompletions.ts
index acdf121dd..99281d38b 100644
--- a/src/documents/templates/getDependsOnCompletions.ts
+++ b/src/documents/templates/getDependsOnCompletions.ts
@@ -1,11 +1,18 @@
+// ---------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.md in the project root for license information.
+// ---------------------------------------------------------------------------------------------
+
import { MarkdownString } from "vscode";
+import { templateKeys } from "../../constants";
import { assert } from "../../fixed_assert";
+import { isTleExpression } from "../../language/expressions/TLE";
import * as Json from "../../language/json/JSON";
import { ContainsBehavior, Span } from "../../language/Span";
import { isSingleQuoted, removeSingleQuotes } from "../../util/strings";
import * as Completion from "../../vscodeIntegration/Completion";
import { TemplatePositionContext } from "../positionContexts/TemplatePositionContext";
-import { getResourcesInfo, IJsonResourceInfo, IResourceInfo } from "./getResourcesInfo";
+import { getResourcesInfo, IJsonResourceInfo, IResourceInfo, jsonStringToTleExpression } from "./getResourcesInfo";
// Handle completions for dependsOn array entries
export function getDependsOnCompletions(
@@ -39,10 +46,8 @@ export function getDependsOnCompletions(
continue;
}
- const item = getDependsOnCompletion(resource, span);
- if (item) {
- completions.push(item);
- }
+ const items = getDependsOnCompletionsForResource(resource, span);
+ completions.push(...items);
}
return completions;
@@ -51,20 +56,23 @@ export function getDependsOnCompletions(
/**
* Get possible completions for entries inside a resource's "dependsOn" array
*/
-function getDependsOnCompletion(resource: IResourceInfo, span: Span): Completion.Item | undefined {
+function getDependsOnCompletionsForResource(resource: IJsonResourceInfo, span: Span): Completion.Item[] {
+ const completions: Completion.Item[] = [];
+
const resourceIdExpression = resource.getResourceIdExpression();
const shortNameExpression = resource.shortNameExpression;
if (shortNameExpression && resourceIdExpression) {
const label = shortNameExpression;
- let typeExpression = resource.getFullTypeExpression();
- if (typeExpression && isSingleQuoted(typeExpression)) {
+ const fullTypeExpression = resource.getFullTypeExpression();
+ let shortTypeExpression = fullTypeExpression;
+ if (shortTypeExpression && isSingleQuoted(shortTypeExpression)) {
// Simplify the type expression to remove quotes and the first prefix (e.g. 'Microsoft.Compute/')
- typeExpression = removeSingleQuotes(typeExpression);
- typeExpression = typeExpression.replace(/^[^/]+\//, '');
+ shortTypeExpression = removeSingleQuotes(shortTypeExpression);
+ shortTypeExpression = shortTypeExpression.replace(/^[^/]+\//, '');
}
const insertText = `"[${resourceIdExpression}]"`;
- const detail = typeExpression;
+ const detail = shortTypeExpression;
const documentation = `Inserts this resourceId reference:\n\`\`\`arm-template\n"[${resourceIdExpression}]"\n\`\`\`\n
`;
const item = new Completion.Item({
@@ -79,10 +87,38 @@ function getDependsOnCompletion(resource: IResourceInfo, span: Span): Completion
filterText: insertText
});
- return item;
+ completions.push(item);
+
+ const copyName = resource.copyElement?.getPropertyValue(templateKeys.copyName)?.asStringValue?.unquotedValue;
+ if (copyName) {
+ const copyNameExpression = jsonStringToTleExpression(copyName);
+ const copyLabel = `LOOP ${copyNameExpression}`;
+ const copyInsertText = isTleExpression(copyName) ? `"[${copyNameExpression}]"` : `"${copyName}"`;
+ const copyDetail = detail;
+ // tslint:disable-next-line: prefer-template
+ const copyDocumentation = `Inserts this COPY element reference:
+\`\`\`arm-template
+${copyInsertText}
+\`\`\`
+from resource \`${shortNameExpression}\` of type \`${shortTypeExpression}\``;
+
+ const copyItem = new Completion.Item({
+ label: copyLabel,
+ insertText: copyInsertText,
+ detail: copyDetail,
+ documentation: new MarkdownString(copyDocumentation),
+ span,
+ kind: Completion.CompletionKind.dependsOnResourceCopyLoop,
+ // Normally vscode uses label if this isn't specified, but it doesn't seem to like the "[" in the label,
+ // so specify filter text explicitly
+ filterText: copyInsertText
+ });
+
+ completions.push(copyItem);
+ }
}
- return undefined;
+ return completions;
}
function findClosestEnclosingResource(documentIndex: number, infos: IJsonResourceInfo[]): IJsonResourceInfo | undefined {
diff --git a/src/documents/templates/getResourcesInfo.ts b/src/documents/templates/getResourcesInfo.ts
index 5aaf4df66..e5517cdd3 100644
--- a/src/documents/templates/getResourcesInfo.ts
+++ b/src/documents/templates/getResourcesInfo.ts
@@ -57,6 +57,11 @@ export interface IJsonResourceInfo extends IResourceInfo {
* The JSON object that represents this resource
*/
resourceObject: Json.ObjectValue;
+
+ /**
+ * The COPY element for this resource, if any
+ */
+ copyElement: Json.ObjectValue | undefined;
}
export class ResourceInfo implements IResourceInfo {
@@ -94,6 +99,10 @@ export class JsonResourceInfo extends ResourceInfo implements JsonResourceInfo {
public constructor(nameSegmentExpressions: string[], typeSegmentExpressions: string[], public readonly resourceObject: Json.ObjectValue, parent: IJsonResourceInfo | undefined) {
super(nameSegmentExpressions, typeSegmentExpressions, parent);
}
+
+ public get copyElement(): Json.ObjectValue | undefined {
+ return this.resourceObject.getPropertyValue(templateKeys.copyLoop)?.asObjectValue;
+ }
}
/**
diff --git a/src/vscodeIntegration/Completion.ts b/src/vscodeIntegration/Completion.ts
index 4530e2657..725958b30 100644
--- a/src/vscodeIntegration/Completion.ts
+++ b/src/vscodeIntegration/Completion.ts
@@ -228,6 +228,7 @@ export enum CompletionKind {
// ARM template structure completions
dependsOnResourceId = "dependsOnResourceId", // Completion inside dependsOn of a resourceId reference to a resource
+ dependsOnResourceCopyLoop = "dependsOnResourceCopyLoop", // Completion inside dependsOn of a COPY loop inside a resource
// Snippet
Snippet = "Snippet",
diff --git a/src/vscodeIntegration/toVsCodeCompletionItem.ts b/src/vscodeIntegration/toVsCodeCompletionItem.ts
index f8edcad6d..f3e882b28 100644
--- a/src/vscodeIntegration/toVsCodeCompletionItem.ts
+++ b/src/vscodeIntegration/toVsCodeCompletionItem.ts
@@ -96,6 +96,7 @@ export function toVsCodeCompletionItemKind(kind: Completion.CompletionKind): vsc
case Completion.CompletionKind.tleResourceIdResTypeParameter:
case Completion.CompletionKind.tleResourceIdResNameParameter:
case Completion.CompletionKind.dependsOnResourceId:
+ case Completion.CompletionKind.dependsOnResourceCopyLoop:
return vscode.CompletionItemKind.Reference;
case Completion.CompletionKind.Snippet:
diff --git a/test/dependsOn.completions.test.ts b/test/dependsOn.completions.test.ts
index d5fa42063..02e82bb25 100644
--- a/test/dependsOn.completions.test.ts
+++ b/test/dependsOn.completions.test.ts
@@ -67,14 +67,21 @@ suite("dependsOn completions", () => {
},
{
type: "microsoft.abc/def",
- name: "name1a"
+ name: "name1",
+ copy: {
+ name: "copy"
+ }
}
]
},
expected: [
{
- "label": "'name1a'",
+ "label": "'name1'",
"kind": Completion.CompletionKind.dependsOnResourceId
+ },
+ {
+ "label": "LOOP 'copy'",
+ "kind": Completion.CompletionKind.dependsOnResourceCopyLoop
}
]
}
@@ -124,6 +131,36 @@ suite("dependsOn completions", () => {
}
]
});
+
+ createDependsOnCompletionsTest(
+ "detail for copy loop is short type name",
+ {
+ template: {
+ resources: [
+ {
+ dependsOn: [
+ ""
+ ]
+ },
+ {
+ type: "Microsoft.abc/def",
+ name: "name1",
+ copy: {
+ name: "copyname"
+ }
+ }
+ ]
+ },
+ expected: [
+ {
+ "label": "'name1'"
+ },
+ {
+ "label": "LOOP 'copyname'",
+ "detail": "def"
+ }
+ ]
+ });
});
suite("documentation", () => {
@@ -139,7 +176,10 @@ suite("dependsOn completions", () => {
},
{
type: "microsoft.abc/def",
- name: "name1a"
+ name: "name1a",
+ copy: {
+ name: "copyname"
+ }
}
]
},
@@ -154,6 +194,17 @@ suite("dependsOn completions", () => {
"\"[resourceId('microsoft.abc/def', 'name1a')]\"\n" +
"```\n
"
}
+ },
+ {
+ // tslint:disable-next-line: no-any
+ "documention": {
+ value:
+ `Inserts this COPY element reference:
+\`\`\`arm-template
+"copyname"
+\`\`\`
+from resource \`'name1a'\` of type \`def\``
+ }
}
]
}
@@ -218,6 +269,49 @@ suite("dependsOn completions", () => {
]
}
);
+
+ createDependsOnCompletionsTest(
+ "label for copy loop",
+ {
+ template: {
+ resources: [
+ {
+ dependsOn: [
+ ""
+ ]
+ },
+ {
+ type: "microsoft.abc/def",
+ name: "name1",
+ copy: {
+ name: "copynameliteral"
+ }
+ },
+ {
+ type: "microsoft.abc/def",
+ name: "name2",
+ copy: {
+ name: "[concat('copy', 'name1')]"
+ }
+ }
+ ]
+ },
+ expected: [
+ {
+ "label": `'name1'`
+ },
+ {
+ "label": `LOOP 'copynameliteral'`
+ },
+ {
+ "label": `'name2'`
+ },
+ {
+ "label": `LOOP concat('copy', 'name1')`
+ }
+ ]
+ }
+ );
});
suite("expressions", () => {
@@ -322,6 +416,49 @@ suite("dependsOn completions", () => {
}
);
+ createDependsOnCompletionsTest(
+ "insertionText for copy loop",
+ {
+ template: {
+ resources: [
+ {
+ dependsOn: [
+ ""
+ ]
+ },
+ {
+ type: "microsoft.abc/def",
+ name: "name1",
+ copy: {
+ name: "copynameliteral"
+ }
+ },
+ {
+ type: "microsoft.abc/def",
+ name: "name2",
+ copy: {
+ name: "[concat('copy', 'name1')]"
+ }
+ }
+ ]
+ },
+ expected: [
+ {
+ "label": `'name1'`
+ },
+ {
+ "insertText": `"copynameliteral"`
+ },
+ {
+ "label": `'name2'`
+ },
+ {
+ "insertText": `"[concat('copy', 'name1')]"`
+ }
+ ]
+ }
+ );
+
createDependsOnCompletionsTest(
"name is expression",
{