Skip to content

Commit

Permalink
dependsOn completions, part 1 (microsoft#938)
Browse files Browse the repository at this point in the history
* Add bail after first failure flag

* dependsOn completions, part 1

* fix

* Improve documentation

* default allow out of bounds=true

* clean-up

* suite fixes

* CR fixes
  • Loading branch information
StephenWeatherford authored Sep 3, 2020
1 parent 45fa631 commit f1cd208
Show file tree
Hide file tree
Showing 23 changed files with 1,902 additions and 156 deletions.
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

0 comments on commit f1cd208

Please sign in to comment.