From ac8a1df0ec479c087a2b1a99b2b57cee51d21f93 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 4 Sep 2020 10:03:43 -0700 Subject: [PATCH] Support COPY loops in dependsOn completions (#940) --- .../templates/getDependsOnCompletions.ts | 62 ++++++-- src/documents/templates/getResourcesInfo.ts | 9 ++ src/vscodeIntegration/Completion.ts | 1 + .../toVsCodeCompletionItem.ts | 1 + test/dependsOn.completions.test.ts | 143 +++++++++++++++++- 5 files changed, 200 insertions(+), 16 deletions(-) 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", {