Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dependsOn completions, part 1 #938

Merged
merged 8 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"env": {
"MOCHA_grep": "", // RegExp of tests to run (case-insensitve, set to empty for all)
"MOCHA_invert": "0", // Invert the RegExp
"MOCHA_bail": "0", // Bail after first failure
"AZCODE_ARM_IGNORE_BUNDLE": "1",
"MOCHA_enableTimeouts": "0", // Disable time-outs
"DEBUGTELEMETRY": "1", // 1=quiet; verbose=see telemetry in console; 0=send telemetry
Expand Down Expand Up @@ -99,6 +100,7 @@
"env": {
"MOCHA_grep": "", // RegExp of tests to run (empty for all)
"MOCHA_invert": "0", // Invert the RegExp
"MOCHA_bail": "0", // Bail after first failure
"MOCHA_enableTimeouts": "0", // Disable time-outs
"DEBUGTELEMETRY": "1", // 1=quiet; verbose=see telemetry in console; 0=send telemetry
"NODE_DEBUG": "",
Expand Down
10 changes: 2 additions & 8 deletions extension.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// tslint:disable: no-consecutive-blank-lines // Format-on-save tends to add an extra line to the end of this file

/**
* This is the external face of extension.bundle.js, the main webpack bundle for the extension.
* Anything needing to be exposed outside of the extension sources must be exported from here, because
Expand All @@ -17,11 +15,9 @@
* The tests should import '../extension.bundle.ts'. At design-time they live in tests/ and so will pick up this file (extension.bundle.ts).
* At runtime the tests live in dist/tests and will therefore pick up the main webpack bundle at dist/extension.bundle.js.
*/

import * as TLE from "./src/language/expressions/TLE";
import * as Json from "./src/language/json/JSON";
import * as basic from "./src/language/json/Tokenizer";
import * as Language from "./src/language/LineColPos";
import * as Completion from './src/vscodeIntegration/Completion';

export { activateInternal, deactivateInternal } from './src/AzureRMTools'; // Export activate/deactivate for main.js
Expand All @@ -43,7 +39,7 @@ export { ParameterDefinitionCodeLens, ShowCurrentParameterFileCodeLens } from ".
export { DeploymentTemplateDoc } from "./src/documents/templates/DeploymentTemplateDoc";
export { ExpressionType } from "./src/documents/templates/ExpressionType";
export { looksLikeResourceTypeStringLiteral } from "./src/documents/templates/getResourceIdCompletions";
export { splitResourceNameIntoSegments } from "./src/documents/templates/getResourcesInfo";
export { getResourcesInfo, IResourceInfo, ResourceInfo, splitResourceNameIntoSegments } from "./src/documents/templates/getResourcesInfo";
export { InsertItem } from "./src/documents/templates/insertItem";
export { TemplateScope, TemplateScopeKind } from "./src/documents/templates/scopes/TemplateScope";
export * from "./src/documents/templates/scopes/templateScopes";
Expand All @@ -58,7 +54,7 @@ export { FunctionSignatureHelp, TleParseResult } from "./src/language/expression
export { DefinitionKind, INamedDefinition } from "./src/language/INamedDefinition";
export { Issue } from "./src/language/Issue";
export { IssueKind } from "./src/language/IssueKind";
export * from "./src/language/LineColPos";
export { LineColPos } from "./src/language/LineColPos";
export { ReferenceList } from "./src/language/ReferenceList";
export { ContainsBehavior, Span } from "./src/language/Span";
export { LanguageServerState } from "./src/languageclient/startArmLanguageServer";
Expand Down Expand Up @@ -93,14 +89,12 @@ export { UndefinedVariablePropertyVisitor } from "./src/visitors/UndefinedVariab
export { UnrecognizedBuiltinFunctionIssue, UnrecognizedUserFunctionIssue, UnrecognizedUserNamespaceIssue } from "./src/visitors/UnrecognizedFunctionIssues";
export { UnrecognizedFunctionVisitor } from "./src/visitors/UnrecognizedFunctionVisitor";
export { IGotoParameterValueArgs } from "./src/vscodeIntegration/commandArguments";
export * from "./src/vscodeIntegration/Completion";
export { IConfiguration } from "./src/vscodeIntegration/Configuration";
export { HoverInfo } from "./src/vscodeIntegration/Hover";
export { JsonOutlineProvider, shortenTreeLabel } from "./src/vscodeIntegration/Treeview";
export { getVSCodePositionFromPosition, getVSCodeRangeFromSpan } from "./src/vscodeIntegration/vscodePosition";
export { Completion };
export { Json };
export { Language };
export { basic };
export { TLE };

72 changes: 52 additions & 20 deletions src/documents/positionContexts/PositionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,24 @@ export abstract class PositionContext {
* the span of insertion, etc.)
*/
// tslint:disable-next-line: max-func-body-length cyclomatic-complexity
public getInsertionContext(triggerCharacter: string | undefined): InsertionContext {
public getInsertionContext(options: { triggerCharacter?: string; allowInsideJsonString?: boolean }): InsertionContext {
const triggerCharacter = options.triggerCharacter;
const allowInsideJsonString = !!options.allowInsideJsonString;

if (!this.document.topLevelValue) {
// Empty JSON document
return { context: KnownContexts.emptyDocument, parents: [] };
}

let replacementInfo = this.getCompletionReplacementSpanInfo();
const insideDoubleQuotes = replacementInfo.token?.type === Json.TokenType.QuotedString;
const insideJsonString = this.jsonToken?.type === Json.TokenType.QuotedString;
let parents: (Json.ArrayValue | Json.ObjectValue | Json.Property)[] = [];

const insertionParent: Json.ArrayValue | Json.ObjectValue | undefined = this.getInsertionParent();
let insertionParent: Json.ArrayValue | Json.ObjectValue | undefined = this.getInsertionParent();
if (insideJsonString && !insertionParent && allowInsideJsonString) {
const pcAtStartOfString = this.document.getContextFromDocumentCharacterIndex(this.jsonTokenStartIndex, this._associatedDocument);
insertionParent = pcAtStartOfString.getInsertionParent();
}

if (insertionParent) {
const lineage: (Json.ArrayValue | Json.ObjectValue | Json.Property)[] | undefined = this.document.topLevelValue.findLineage(insertionParent);
assert(lineage, `Couldn't find JSON value inside the top-level value: ${insertionParent.toFullFriendlyString()}`);
Expand All @@ -296,13 +303,16 @@ export abstract class PositionContext {
// - context is "parameters"
// }
//
const parentPropertyName = parents[1].propertyName;
return { context: parentPropertyName, parents, insideDoubleQuotes };
const parentPropertyName = parents[1].propertyName?.toLowerCase();
return { context: parentPropertyName, parents, insideJsonString };
}

if (
!insideDoubleQuotes
&& !triggerCharacter
(
(insideJsonString && allowInsideJsonString)
|| !insideJsonString
)
&& (!triggerCharacter || triggerCharacter === '"')
&& parents[0] instanceof Json.ArrayValue
&& parents[1] instanceof Json.Property
) {
Expand All @@ -315,12 +325,12 @@ export abstract class PositionContext {
// - context is "resources"
// ]
//
const parentPropertyName = parents[1].propertyName;
return { context: parentPropertyName, parents };
const parentPropertyName = parents[1].propertyName?.toLowerCase();
return { context: parentPropertyName, parents, insideJsonString };
}

if (
!insideDoubleQuotes
!insideJsonString // Don't currently need the insideJsonString=true case here
&& parents[0] instanceof Json.ObjectValue
&& parents[1] instanceof Json.ArrayValue
&& parents[2] instanceof Json.Property
Expand All @@ -345,7 +355,8 @@ export abstract class PositionContext {
return {
context: undefined,
triggerSuggest: true,
parents
parents,
insideJsonString
};
} else if (!triggerCharacter) {
// Inside an empty object inside an array (likely because of the above case but could be manually triggered
Expand All @@ -360,21 +371,29 @@ export abstract class PositionContext {
// }
// ]
//
const parentPropertyName = parents[2].propertyName;
const parentPropertyName = parents[2].propertyName?.toLowerCase();
return {
context: parentPropertyName,
curlyBraces: insertionParent.span,
parents
parents,
insideJsonString
};
}
}
}

return { context: undefined, parents, insideDoubleQuotes };
return { context: undefined, parents, insideJsonString };
}

public get isInsideComment(): boolean {
return !!this.document.jsonParseResult.getCommentTokenAtDocumentIndex(
this.documentCharacterIndex,
ContainsBehavior.enclosed);
}

// Retrieves the array or object which would be the parent if a JSON item were added
// at the current location
// at the current location. If the cursor is inside any other kind of value, or inside a comment,
// returns undefined.
public getInsertionParent(): Json.ObjectValue | Json.ArrayValue | undefined {
const enclosingJsonValue = this.document.jsonParseResult.getValueAtCharacterIndex(
this.documentCharacterIndex,
Expand All @@ -385,14 +404,27 @@ export abstract class PositionContext {

// We're immediately inside an object or array, not any other kind of value. But we could still be inside
// a comment
if (!!this.document.jsonParseResult.getCommentTokenAtDocumentIndex(
this.documentCharacterIndex,
ContainsBehavior.enclosed)
) {
if (this.isInsideComment) {
// Inside a comment, can't insert here!
return undefined;
}

return enclosingJsonValue;
}

/**
* Gets the nearest enclosing parent (array or object) at the current position
*/
public getEnclosingParent(): Json.ArrayValue | Json.ObjectValue | undefined {
const enclosingJsonValue = this.document.jsonParseResult.getValueAtCharacterIndex(
this.documentCharacterIndex,
ContainsBehavior.enclosed);
if (enclosingJsonValue) {
const lineage: (Json.ArrayValue | Json.ObjectValue | Json.Property)[] | undefined = this.document.topLevelValue?.findLineage(enclosingJsonValue) ?? [];
const lineageWithoutProperties = <(Json.ArrayValue | Json.ObjectValue)[]>lineage.filter(l => !(l instanceof Json.Property));
return lineageWithoutProperties[lineage.length - 1];
}

return undefined;
}
}
51 changes: 39 additions & 12 deletions src/documents/positionContexts/TemplatePositionContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// ----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ----------------------------------------------------------------------------

// tslint:disable:max-line-length

import { templateKeys } from "../../constants";
import { ext } from "../../extensionVariables";
import { assert } from '../../fixed_assert';
Expand All @@ -21,6 +19,7 @@ import { DeploymentParametersDoc } from "../parameters/DeploymentParametersDoc";
import { IParameterDefinition } from "../parameters/IParameterDefinition";
import { getPropertyValueCompletionItems } from "../parameters/ParameterValues";
import { DeploymentTemplateDoc } from "../templates/DeploymentTemplateDoc";
import { getDependsOnCompletions } from "../templates/getDependsOnCompletions";
import { getResourceIdCompletions } from "../templates/getResourceIdCompletions";
import { IFunctionMetadata, IFunctionParameterMetadata } from "../templates/IFunctionMetadata";
import { TemplateScope } from "../templates/scopes/TemplateScope";
Expand Down Expand Up @@ -51,13 +50,13 @@ class TleInfo implements ITleInfo {
export class TemplatePositionContext extends PositionContext {
private _tleInfo: CachedValue<TleInfo | undefined> = new CachedValue<TleInfo | undefined>();

public static fromDocumentLineAndColumnIndexes(deploymentTemplate: DeploymentTemplateDoc, documentLineIndex: number, documentColumnIndex: number, associatedParameters: DeploymentParametersDoc | undefined, allowOutOfBounds: boolean = false): TemplatePositionContext {
public static fromDocumentLineAndColumnIndexes(deploymentTemplate: DeploymentTemplateDoc, documentLineIndex: number, documentColumnIndex: number, associatedParameters: DeploymentParametersDoc | undefined, allowOutOfBounds: boolean = true): TemplatePositionContext {
let context = new TemplatePositionContext(deploymentTemplate, associatedParameters);
context.initFromDocumentLineAndColumnIndices(documentLineIndex, documentColumnIndex, allowOutOfBounds);
return context;
}

public static fromDocumentCharacterIndex(deploymentTemplate: DeploymentTemplateDoc, documentCharacterIndex: number, associatedParameters: DeploymentParametersDoc | undefined, allowOutOfBounds: boolean = false): TemplatePositionContext {
public static fromDocumentCharacterIndex(deploymentTemplate: DeploymentTemplateDoc, documentCharacterIndex: number, associatedParameters: DeploymentParametersDoc | undefined, allowOutOfBounds: boolean = true): TemplatePositionContext {
let context = new TemplatePositionContext(deploymentTemplate, associatedParameters);
context.initFromDocumentCharacterIndex(documentCharacterIndex, allowOutOfBounds);
return context;
Expand Down Expand Up @@ -287,8 +286,8 @@ export class TemplatePositionContext extends PositionContext {
return false;
}

public getInsertionContext(triggerCharacter: string | undefined): InsertionContext {
const insertionContext = super.getInsertionContext(triggerCharacter);
public getInsertionContext(options: { triggerCharacter?: string; allowInsideJsonString?: boolean }): InsertionContext {
const insertionContext = super.getInsertionContext(options);
const context = insertionContext.context;
const parents = insertionContext.parents;

Expand All @@ -304,7 +303,7 @@ export class TemplatePositionContext extends PositionContext {
insertionContext.context = KnownContexts.userFuncParameterDefinitions;
}
} else if (
(triggerCharacter === undefined || triggerCharacter === '"')
(options.triggerCharacter === undefined || options.triggerCharacter === '"')
&& this.isInsideResourceBody(parents)
) {
insertionContext.context = KnownContexts.resourceBody;
Expand All @@ -329,17 +328,14 @@ export class TemplatePositionContext extends PositionContext {
}

if (!tleInfo) {
// No TLE string at this location, consider snippet completions
// No JSON string at this location, consider snippet completions
const snippets = await this.getSnippetCompletionItems(triggerCharacter);
if (snippets.triggerSuggest) {
return snippets;
} else {
completions.push(...snippets.items);
}
} else {

// 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;
Expand All @@ -366,11 +362,42 @@ export class TemplatePositionContext extends PositionContext {
}
}

completions.push(...this.getDependsOnCompletionItems(triggerCharacter));

return { items: completions };
}

/**
* Gets the scope at the current position
*/
public getScope(): TemplateScope {
if (this.jsonValue && this.document.topLevelValue) {
const objectLineage = <(Json.ObjectValue | Json.ArrayValue)[]>this.document.topLevelValue
?.findLineage(this.jsonValue)
?.filter(v => v instanceof Json.ObjectValue);
const scopes = this.document.allScopes; // Note: not unique because resources are unique even when scope is not (see CONSIDER in TemplateScope.ts)
for (const parent of objectLineage.reverse()) {
const innermostMachingScope = scopes.find(s => s.rootObject === parent);
if (innermostMachingScope) {
return innermostMachingScope;
}
}
}

return this.document.topLevelScope;
}

private getDependsOnCompletionItems(triggerCharacter: string | undefined): Completion.Item[] {
const insertionContext = this.getInsertionContext({ triggerCharacter, allowInsideJsonString: true });
if (insertionContext.context === 'dependson') {
return getDependsOnCompletions(this);
}

return [];
}

private async getSnippetCompletionItems(triggerCharacter: string | undefined): Promise<ICompletionItemsResult> {
const insertionContext = this.getInsertionContext(triggerCharacter);
const insertionContext = this.getInsertionContext({ triggerCharacter });
if (insertionContext.triggerSuggest) {
return { items: [], triggerSuggest: true };
} else if (insertionContext.context) {
Expand Down
4 changes: 2 additions & 2 deletions src/documents/templates/DeploymentTemplateDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,11 @@ export class DeploymentTemplateDoc extends DeploymentDocument {

//#endregion

public getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number, associatedParameters: DeploymentDocument | undefined, allowOutOfBounds: boolean = false): TemplatePositionContext {
public getContextFromDocumentLineAndColumnIndexes(documentLineIndex: number, documentColumnIndex: number, associatedParameters: DeploymentDocument | undefined, allowOutOfBounds: boolean = true): TemplatePositionContext {
return TemplatePositionContext.fromDocumentLineAndColumnIndexes(this, documentLineIndex, documentColumnIndex, expectParameterDocumentOrUndefined(associatedParameters), allowOutOfBounds);
}

public getContextFromDocumentCharacterIndex(documentCharacterIndex: number, associatedParameters: DeploymentDocument | undefined, allowOutOfBounds: boolean = false): TemplatePositionContext {
public getContextFromDocumentCharacterIndex(documentCharacterIndex: number, associatedParameters: DeploymentDocument | undefined, allowOutOfBounds: boolean = true): TemplatePositionContext {
return TemplatePositionContext.fromDocumentCharacterIndex(this, documentCharacterIndex, expectParameterDocumentOrUndefined(associatedParameters), allowOutOfBounds);
}

Expand Down
5 changes: 5 additions & 0 deletions src/documents/templates/IResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ export interface IResource {
* The nameValue of the resource object in the JSON
*/
nameValue: Json.StringValue | undefined;

/**
* The typeValue of the resource object in the JSON
*/
resourceTypeValue: Json.StringValue | undefined;
}
Loading