Skip to content

Commit

Permalink
Support COPY loops in dependsOn completions (#940)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenWeatherford authored Sep 4, 2020
1 parent cdb69ff commit ac8a1df
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 16 deletions.
62 changes: 49 additions & 13 deletions src/documents/templates/getDependsOnCompletions.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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<br/>`;

const item = new Completion.Item({
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/documents/templates/getResourcesInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/vscodeIntegration/Completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/vscodeIntegration/toVsCodeCompletionItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
143 changes: 140 additions & 3 deletions test/dependsOn.completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
Expand Down Expand Up @@ -124,6 +131,36 @@ suite("dependsOn completions", () => {
}
]
});

createDependsOnCompletionsTest(
"detail for copy loop is short type name",
{
template: {
resources: [
{
dependsOn: [
"<!replaceStart!><!cursor!>"
]
},
{
type: "Microsoft.abc/def",
name: "name1",
copy: {
name: "copyname"
}
}
]
},
expected: [
{
"label": "'name1'"
},
{
"label": "LOOP 'copyname'",
"detail": "def"
}
]
});
});

suite("documentation", () => {
Expand All @@ -139,7 +176,10 @@ suite("dependsOn completions", () => {
},
{
type: "microsoft.abc/def",
name: "name1a"
name: "name1a",
copy: {
name: "copyname"
}
}
]
},
Expand All @@ -154,6 +194,17 @@ suite("dependsOn completions", () => {
"\"[resourceId('microsoft.abc/def', 'name1a')]\"\n" +
"```\n<br/>"
}
},
{
// tslint:disable-next-line: no-any
"documention": <any>{
value:
`Inserts this COPY element reference:
\`\`\`arm-template
"copyname"
\`\`\`
from resource \`'name1a'\` of type \`def\``
}
}
]
}
Expand Down Expand Up @@ -218,6 +269,49 @@ suite("dependsOn completions", () => {
]
}
);

createDependsOnCompletionsTest(
"label for copy loop",
{
template: {
resources: [
{
dependsOn: [
"<!replaceStart!><!cursor!>"
]
},
{
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", () => {
Expand Down Expand Up @@ -322,6 +416,49 @@ suite("dependsOn completions", () => {
}
);

createDependsOnCompletionsTest(
"insertionText for copy loop",
{
template: {
resources: [
{
dependsOn: [
"<!replaceStart!><!cursor!>"
]
},
{
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",
{
Expand Down

0 comments on commit ac8a1df

Please sign in to comment.