Skip to content

Commit

Permalink
Templates editing updates (#168)
Browse files Browse the repository at this point in the history
* Add datasetFileTemplate to rules appliesTo

* Add tile visibility to json schema

This is available in the Spring '24 release.

* Warn on empty validate page group

* Add Component page layout type

This is available in the Spring '24 release.

* Remove non-arrow-function return

to make our old tslint version happy, plus we don't need to support propagating
'this' here (and someone could just use a bound method if they did).

* Fix json path matching to be exact matching

The builtin matches() method from jsonc actually does the equivalent of a
starts-with, while in most cases we want to match on an exact json path (e.g.
right to a field, and not to a subfield of the final part of the path pattern).
  • Loading branch information
smithgp committed Jan 9, 2024
1 parent 86d33a8 commit afdfe9f
Show file tree
Hide file tree
Showing 36 changed files with 566 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location, Node as JsonNode } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefCompletionItemProviderDelegate } from '../variables';

Expand All @@ -26,11 +27,9 @@ export class AutoInstallVariableCompletionItemProviderDelegate extends VariableR

public isSupportedLocation(location: Location) {
return (
location.isAtPropertyKey &&
location.matches(['configuration', 'appConfiguration', 'values', '*']) &&
// this makes sure the completion only show in prop names directly under "values" (and not in prop names in object
// makes sure the completion only show in prop names directly under "values" (and not in prop names in object
// values under "values")
location.path.length === 4
location.isAtPropertyKey && locationMatches(location, ['configuration', 'appConfiguration', 'values', '*'])
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefDefinitionProvider } from '../variables';

Expand All @@ -31,7 +32,7 @@ export class AutoInstallVariableDefinitionProvider extends VariableRefDefinition
location.previousNode?.type === 'property' &&
location.previousNode.value &&
// and that it's in a variable name field
location.matches(['configuration', 'appConfiguration', 'values', '*'])
locationMatches(location, ['configuration', 'appConfiguration', 'values', '*'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { VariableRefHoverProvider } from '../variables';

/** Get hover text for a variable name in the appConfiguration.values of an auto-install.json. */
Expand All @@ -24,7 +25,7 @@ export class AutoInstallVariableHoverProvider extends VariableRefHoverProvider {
return (
location.isAtPropertyKey &&
location.previousNode?.type === 'property' &&
location.matches(['configuration', 'appConfiguration', 'values', '*'])
locationMatches(location, ['configuration', 'appConfiguration', 'values', '*'])
);
}
}
14 changes: 11 additions & 3 deletions extensions/analyticsdx-vscode-templates/src/layout/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import * as vscode from 'vscode';
import { codeCompletionUsedTelemetryCommand } from '../telemetry';
import { TemplateDirEditing } from '../templateEditing';
import { JsonCompletionItemProviderDelegate, newCompletionItem } from '../util/completions';
import { locationMatches } from '../util/jsoncUtils';
import { isValidVariableName } from '../util/templateUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefCompletionItemProviderDelegate } from '../variables';
import { getLayoutItemVariableName, isInTilesEnumKey, matchesLayoutItem } from './utils';
import {
getLayoutItemVariableName,
isInComponentLayoutVariableName,
isInTilesEnumKey,
matchesLayoutItem
} from './utils';

/** Get tags from the readiness file's templateRequirements. */
export class LayoutValidationPageTagCompletionItemProviderDelegate implements JsonCompletionItemProviderDelegate {
Expand All @@ -34,7 +40,7 @@ export class LayoutValidationPageTagCompletionItemProviderDelegate implements Js
// get the parent node hierarchy in the Location passed in, and it's not that big a deal if the user gets a
// code-completion for this path in the layout.json file on a Configuration page since they'll already be getting
// errors about the wrong type
return !location.isAtPropertyKey && location.matches(['pages', '*', 'groups', '*', 'tags', '*']);
return !location.isAtPropertyKey && locationMatches(location, ['pages', '*', 'groups', '*', 'tags', '*']);
}

public async getItems(range: vscode.Range | undefined, location: Location, document: vscode.TextDocument) {
Expand Down Expand Up @@ -77,7 +83,9 @@ export class LayoutVariableCompletionItemProviderDelegate extends VariableRefCom

public override isSupportedLocation(location: Location, context: vscode.CompletionContext): boolean {
// make sure that it's in a variable name value
return !location.isAtPropertyKey && matchesLayoutItem(location, 'name');
return (
!location.isAtPropertyKey && (isInComponentLayoutVariableName(location) || matchesLayoutItem(location, 'name'))
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { matchJsonNodeAtPattern } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { rangeForNode } from '../util/vscodeUtils';
import { VariableRefDefinitionProvider } from '../variables';
import { getLayoutItemVariableName, isInTilesEnumKey, matchesLayoutItem } from './utils';
import {
getLayoutItemVariableName,
isInComponentLayoutVariableName,
isInTilesEnumKey,
matchesLayoutItem
} from './utils';

/** Handle CMD+Click from a variable name in layout.json to the variable in variables.json. */
export class LayoutVariableDefinitionProvider extends VariableRefDefinitionProvider {
Expand All @@ -37,7 +42,7 @@ export class LayoutVariableDefinitionProvider extends VariableRefDefinitionProvi
location.previousNode?.type === 'string' &&
location.previousNode.value &&
// and that it's in a variable name field
matchesLayoutItem(location, 'name')
(isInComponentLayoutVariableName(location) || matchesLayoutItem(location, 'name'))
);
}
}
Expand Down
8 changes: 6 additions & 2 deletions extensions/analyticsdx-vscode-templates/src/layout/hovers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { VariableRefHoverProvider } from '../variables';
import { matchesLayoutItem } from './utils';
import { isInComponentLayoutVariableName, matchesLayoutItem } from './utils';

/** Get hover text for a variable from the name in a page in a layout.json file. */
export class LayoutVariableHoverProvider extends VariableRefHoverProvider {
Expand All @@ -22,6 +22,10 @@ export class LayoutVariableHoverProvider extends VariableRefHoverProvider {
}

protected override isSupportedLocation(location: Location) {
return !location.isAtPropertyKey && location.previousNode?.type === 'string' && matchesLayoutItem(location, 'name');
return (
!location.isAtPropertyKey &&
location.previousNode?.type === 'string' &&
(isInComponentLayoutVariableName(location) || matchesLayoutItem(location, 'name'))
);
}
}
15 changes: 11 additions & 4 deletions extensions/analyticsdx-vscode-templates/src/layout/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { JSONPath, Location, parseTree } from 'jsonc-parser';
import * as vscode from 'vscode';
import { matchJsonNodeAtPattern } from '../util/jsoncUtils';
import { locationMatches, matchJsonNodeAtPattern } from '../util/jsoncUtils';

const paths: Array<Readonly<JSONPath>> = [
['pages', '*', 'layout', 'center', 'items', '*'],
Expand All @@ -21,16 +21,23 @@ const paths: Array<Readonly<JSONPath>> = [
/** Tell if the specified json location is in a layout item
* @param location the location
* @param attrName the name of an item attribute to also check.
* @param exact true (default) to exactly match the item (or item attribute) path, false to just be at or under.
*/
export function matchesLayoutItem(location: Location, attrName?: string) {
export function matchesLayoutItem(location: Location, attrName?: string, exact = true) {
// TODO: make this more specific to the layout type (e.g. only 'center' if SingleColumn)
return paths.some(path => location.matches(attrName ? path.concat(attrName) : (path as JSONPath)));
return paths.some(path => locationMatches(location, attrName ? path.concat(attrName) : (path as JSONPath), exact));
}

/** Tell if the specified json location is in a `Component` layout's variable's name field. */
export function isInComponentLayoutVariableName(location: Location) {
return locationMatches(location, ['pages', '*', 'layout', 'variables', '*', 'name']);
}

export function isInTilesEnumKey(location: Location) {
return (
location.isAtPropertyKey &&
matchesLayoutItem(location, 'tiles') &&
// do non-exact match on the location to handle the jsonpath when in a key
matchesLayoutItem(location, 'tiles', false) &&
// when it's directly in the keys of 'tiles', then the path will be like [..., 'tiles', ''] or
// [..., 'tiles', 'enumValue'], so only trigger then (to avoid triggering when down the tree in the
// tile def objects). also, check the path length to avoid triggering when a tile enumValue is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location, Node as JsonNode } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefCompletionItemProviderDelegate } from '../variables';

Expand All @@ -27,10 +28,9 @@ export class ReadinessVariableCompletionItemProviderDelegate extends VariableRef
public isSupportedLocation(location: Location) {
return (
location.isAtPropertyKey &&
location.matches(['values', '*']) &&
// this makes sure the completion only show in prop names directly under "values" (and not in prop names in object
// values under "values")
location.path.length === 2
locationMatches(location, ['values', '*'])
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefDefinitionProvider } from '../variables';

Expand All @@ -31,7 +32,7 @@ export class ReadinessVariableDefinitionProvider extends VariableRefDefinitionPr
location.previousNode?.type === 'property' &&
location.previousNode.value &&
// and that it's in a variable name field in values
location.matches(['values', '*'])
locationMatches(location, ['values', '*'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { VariableRefHoverProvider } from '../variables';

/** Get hover text for a variable name in the values of a readiness.json. */
Expand All @@ -21,6 +22,10 @@ export class ReadinessVariableHoverProvider extends VariableRefHoverProvider {
}

protected isSupportedLocation(location: Location) {
return location.isAtPropertyKey && location.previousNode?.type === 'property' && location.matches(['values', '*']);
return (
location.isAtPropertyKey &&
location.previousNode?.type === 'property' &&
locationMatches(location, ['values', '*'])
);
}
}
14 changes: 9 additions & 5 deletions extensions/analyticsdx-vscode-templates/src/templateEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
import { JsonCompletionItemProvider, newRelativeFilepathDelegate } from './util/completions';
import { JsonAttributeRelFilePathDefinitionProvider } from './util/definitions';
import { Disposable } from './util/disposable';
import { matchJsonNodesAtPattern } from './util/jsoncUtils';
import { locationMatches, matchJsonNodesAtPattern } from './util/jsoncUtils';
import { Logger, PrefixingOutputChannel } from './util/logger';
import { findTemplateInfoFileFor } from './util/templateUtils';
import { isValidRelpath } from './util/utils';
Expand Down Expand Up @@ -276,22 +276,26 @@ export class TemplateDirEditing extends Disposable {
const fileCompleter = new JsonCompletionItemProvider(
// locations that support *.json fies:
newRelativeFilepathDelegate({
isSupportedLocation: l => !l.isAtPropertyKey && TEMPLATE_INFO.jsonRelFilePathLocationPatterns.some(l.matches),
isSupportedLocation: l =>
!l.isAtPropertyKey && TEMPLATE_INFO.jsonRelFilePathLocationPatterns.some(p => locationMatches(l, p)),
filter: templateJsonFileFilter
}),
// attributes that should have html paths
newRelativeFilepathDelegate({
isSupportedLocation: l => !l.isAtPropertyKey && TEMPLATE_INFO.htmlRelFilePathLocationPatterns.some(l.matches),
isSupportedLocation: l =>
!l.isAtPropertyKey && TEMPLATE_INFO.htmlRelFilePathLocationPatterns.some(p => locationMatches(l, p)),
filter: htmlFileFilter
}),
// attribute that should point to images
newRelativeFilepathDelegate({
isSupportedLocation: l => !l.isAtPropertyKey && TEMPLATE_INFO.imageRelFilePathLocationPatterns.some(l.matches),
isSupportedLocation: l =>
!l.isAtPropertyKey && TEMPLATE_INFO.imageRelFilePathLocationPatterns.some(p => locationMatches(l, p)),
filter: imageFileFilter
}),
// the file in externalFiles should be a .csv
newRelativeFilepathDelegate({
isSupportedLocation: l => !l.isAtPropertyKey && TEMPLATE_INFO.csvRelFilePathLocationPatterns.some(l.matches),
isSupportedLocation: l =>
!l.isAtPropertyKey && TEMPLATE_INFO.csvRelFilePathLocationPatterns.some(p => locationMatches(l, p)),
filter: csvFileFilter
}),
// dataModeObjects' dataset field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { Location, parseTree } from 'jsonc-parser';
import * as vscode from 'vscode';
import { codeCompletionUsedTelemetryCommand } from '../telemetry';
import { JsonCompletionItemProviderDelegate, newCompletionItem } from '../util/completions';
import { locationMatches } from '../util/jsoncUtils';

/** Provide completion items for a dataModelObject dataset field, from the datasetFiles' names. */
export class DMODatasetCompletionItemProviderDelegate implements JsonCompletionItemProviderDelegate {
public isSupportedLocation(location: Location) {
return !location.isAtPropertyKey && location.matches(['dataModelObjects', '*', 'dataset']);
return !location.isAtPropertyKey && locationMatches(location, ['dataModelObjects', '*', 'dataset']);
}

public getItems(range: vscode.Range | undefined, location: Location, document: vscode.TextDocument) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { matchJsonNodesAtPattern } from '@salesforce/analyticsdx-template-lint';
import { Location, parseTree } from 'jsonc-parser';
import * as vscode from 'vscode';
import { locationMatches } from '../util/jsoncUtils';
import { rangeForNode } from '../util/vscodeUtils';
import { JsonAttributeDefinitionProvider } from './../util/definitions';

Expand All @@ -28,7 +29,7 @@ export class DMODatasetDefinitionProvider extends JsonAttributeDefinitionProvide
!location.isAtPropertyKey &&
location.previousNode?.type === 'string' &&
location.previousNode.value &&
location.matches(['dataModelObjects', '*', 'dataset'])
locationMatches(location, ['dataModelObjects', '*', 'dataset'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefCompletionItemProviderDelegate } from '../variables';

Expand All @@ -29,7 +30,7 @@ export class UiVariableCompletionItemProviderDelegate extends VariableRefComplet
public isSupportedLocation(location: Location, context: vscode.CompletionContext): boolean {
return (
// make that it's in a variable name value
!location.isAtPropertyKey && location.matches(['pages', '*', 'variables', '*', 'name'])
!location.isAtPropertyKey && locationMatches(location, ['pages', '*', 'variables', '*', 'name'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { isValidRelpath } from '../util/utils';
import { VariableRefDefinitionProvider } from '../variables';

Expand All @@ -32,7 +33,7 @@ export class UiVariableDefinitionProvider extends VariableRefDefinitionProvider
location.previousNode?.type === 'string' &&
location.previousNode.value &&
// and that it's in a variable name field
location.matches(['pages', '*', 'variables', '*', 'name'])
locationMatches(location, ['pages', '*', 'variables', '*', 'name'])
);
}
}
3 changes: 2 additions & 1 deletion extensions/analyticsdx-vscode-templates/src/ui/hovers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Location } from 'jsonc-parser';
import * as vscode from 'vscode';
import { TemplateDirEditing } from '../templateEditing';
import { locationMatches } from '../util/jsoncUtils';
import { VariableRefHoverProvider } from '../variables';

/** Get hover text for a variable from the name in a page in a ui.json file. */
Expand All @@ -24,7 +25,7 @@ export class UiVariableHoverProvider extends VariableRefHoverProvider {
return (
!location.isAtPropertyKey &&
location.previousNode?.type === 'string' &&
location.matches(['pages', '*', 'variables', '*', 'name'])
locationMatches(location, ['pages', '*', 'variables', '*', 'name'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { getLocation, JSONPath, Location } from 'jsonc-parser';
import { posix as path } from 'path';
import * as vscode from 'vscode';
import { locationMatches } from './jsoncUtils';
import { isValidRelpath } from './utils';

/** Base class for providing definition support on fields in a json file. */
Expand Down Expand Up @@ -76,7 +77,7 @@ export class JsonAttributeRelFilePathDefinitionProvider extends JsonAttributeDef
location.previousNode &&
location.previousNode.type === 'string' &&
location.previousNode.value &&
this.patterns.some(location.matches)
this.patterns.some(p => locationMatches(location, p))
);
}
}
15 changes: 14 additions & 1 deletion extensions/analyticsdx-vscode-templates/src/util/jsoncUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { FormattingOptions, getNodePath, JSONPath, Node as JsonNode } from 'jsonc-parser';
import { FormattingOptions, getNodePath, JSONPath, Location, Node as JsonNode } from 'jsonc-parser';

export {
jsonPathToString,
matchJsonNodeAtPattern,
matchJsonNodesAtPattern
} from '@salesforce/analyticsdx-template-lint';

/**
* Matches the location's path against a pattern consisting of strings (for properties) and numbers (for array indices).
* '*' will match a single segment of any property name or index.
* '**' will match a sequence of segments of any property name or index, or no segment.
* @param location the location.
* @param jsonpath the path pattern to match against.
* @param exact true (default) to exactly match the jsonpath length as well (doesn't work with '**'), or false
* to check that the location starts with the jsonpath.
*/
export function locationMatches(location: Location, jsonpath: JSONPath, exact = true) {
return location.matches(jsonpath) && (!exact || location.path.length === jsonpath.length);
}

/** Find the ancestor 'property' json node at or above the specified node for the specified property.
* This does not support wildcard paths.
* @param node the selected node.
Expand Down

0 comments on commit afdfe9f

Please sign in to comment.