refactor(language-service): Add quick info for built in control flow/blocks (#52386)

Adds hover info for:

* Defer blocks
* Triggers and trigger behavior keywords
* For loop empty block
* Track keyword in for loop block

resolves https://github.com/angular/vscode-ng-language-service/issues/1946

PR Close #52386
This commit is contained in:
Andrew Scott 2023-10-20 13:27:17 -07:00 committed by Alex Rickabaugh
parent 73c5d1c04a
commit bf5bda448f
13 changed files with 577 additions and 167 deletions

View file

@ -118,7 +118,9 @@ export class Element implements Node {
}
export abstract class DeferredTrigger implements Node {
constructor(public sourceSpan: ParseSourceSpan) {}
constructor(
public nameSpan: ParseSourceSpan|null, public sourceSpan: ParseSourceSpan,
public prefetchSpan: ParseSourceSpan|null, public whenOrOnSourceSpan: ParseSourceSpan|null) {}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitDeferredTrigger(this);
@ -126,8 +128,12 @@ export abstract class DeferredTrigger implements Node {
}
export class BoundDeferredTrigger extends DeferredTrigger {
constructor(public value: AST, sourceSpan: ParseSourceSpan) {
super(sourceSpan);
constructor(
public value: AST, sourceSpan: ParseSourceSpan, prefetchSpan: ParseSourceSpan|null,
whenSourceSpan: ParseSourceSpan) {
// BoundDeferredTrigger is for 'when' triggers. These aren't really "triggers" and don't have a
// nameSpan. Trigger names are the built in event triggers like hover, interaction, etc.
super(/** nameSpan */ null, sourceSpan, prefetchSpan, whenSourceSpan);
}
}
@ -136,54 +142,75 @@ export class IdleDeferredTrigger extends DeferredTrigger {}
export class ImmediateDeferredTrigger extends DeferredTrigger {}
export class HoverDeferredTrigger extends DeferredTrigger {
constructor(public reference: string|null, sourceSpan: ParseSourceSpan) {
super(sourceSpan);
constructor(
public reference: string|null, nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
}
export class TimerDeferredTrigger extends DeferredTrigger {
constructor(public delay: number, sourceSpan: ParseSourceSpan) {
super(sourceSpan);
constructor(
public delay: number, nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
}
export class InteractionDeferredTrigger extends DeferredTrigger {
constructor(public reference: string|null, sourceSpan: ParseSourceSpan) {
super(sourceSpan);
constructor(
public reference: string|null, nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
}
export class ViewportDeferredTrigger extends DeferredTrigger {
constructor(public reference: string|null, sourceSpan: ParseSourceSpan) {
super(sourceSpan);
constructor(
public reference: string|null, nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
}
export class DeferredBlockPlaceholder implements Node {
export class BlockNode {
constructor(
public children: Node[], public minimumTime: number|null, public sourceSpan: ParseSourceSpan,
public nameSpan: ParseSourceSpan, public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
}
export class DeferredBlockPlaceholder extends BlockNode implements Node {
constructor(
public children: Node[], public minimumTime: number|null, nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitDeferredBlockPlaceholder(this);
}
}
export class DeferredBlockLoading implements Node {
export class DeferredBlockLoading extends BlockNode implements Node {
constructor(
public children: Node[], public afterTime: number|null, public minimumTime: number|null,
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null) {}
nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitDeferredBlockLoading(this);
}
}
export class DeferredBlockError implements Node {
export class DeferredBlockError extends BlockNode implements Node {
constructor(
public children: Node[], public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
public children: Node[], nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitDeferredBlockError(this);
@ -200,7 +227,7 @@ export interface DeferredBlockTriggers {
viewport?: ViewportDeferredTrigger;
}
export class DeferredBlock implements Node {
export class DeferredBlock extends BlockNode implements Node {
readonly triggers: Readonly<DeferredBlockTriggers>;
readonly prefetchTriggers: Readonly<DeferredBlockTriggers>;
private readonly definedTriggers: (keyof DeferredBlockTriggers)[];
@ -210,8 +237,9 @@ export class DeferredBlock implements Node {
public children: Node[], triggers: DeferredBlockTriggers,
prefetchTriggers: DeferredBlockTriggers, public placeholder: DeferredBlockPlaceholder|null,
public loading: DeferredBlockLoading|null, public error: DeferredBlockError|null,
public sourceSpan: ParseSourceSpan, public mainBlockSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {
nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan, public mainBlockSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan|null) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
this.triggers = triggers;
this.prefetchTriggers = prefetchTriggers;
// We cache the keys since we know that they won't change and we
@ -239,25 +267,31 @@ export class DeferredBlock implements Node {
}
}
export class SwitchBlock implements Node {
export class SwitchBlock extends BlockNode implements Node {
constructor(
public expression: AST, public cases: SwitchBlockCase[],
/**
* These blocks are only captured to allow for autocompletion in the language service. They
* aren't meant to be processed in any other way.
*/
public unknownBlocks: UnknownBlock[], public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
public unknownBlocks: UnknownBlock[], sourceSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan|null,
nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitSwitchBlock(this);
}
}
export class SwitchBlockCase implements Node {
export class SwitchBlockCase extends BlockNode implements Node {
constructor(
public expression: AST|null, public children: Node[], public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
public expression: AST|null, public children: Node[], sourceSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan|null,
nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitSwitchBlockCase(this);
@ -270,44 +304,53 @@ export class SwitchBlockCase implements Node {
export type ForLoopBlockContext =
Record<'$index'|'$first'|'$last'|'$even'|'$odd'|'$count', Variable>;
export class ForLoopBlock implements Node {
export class ForLoopBlock extends BlockNode implements Node {
constructor(
public item: Variable, public expression: ASTWithSource, public trackBy: ASTWithSource,
public contextVariables: ForLoopBlockContext, public children: Node[],
public empty: ForLoopBlockEmpty|null, public sourceSpan: ParseSourceSpan,
public mainBlockSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null) {}
public trackKeywordSpan: ParseSourceSpan, public contextVariables: ForLoopBlockContext,
public children: Node[], public empty: ForLoopBlockEmpty|null, sourceSpan: ParseSourceSpan,
public mainBlockSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan|null, nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitForLoopBlock(this);
}
}
export class ForLoopBlockEmpty implements Node {
export class ForLoopBlockEmpty extends BlockNode implements Node {
constructor(
public children: Node[], public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
public children: Node[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan|null, nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitForLoopBlockEmpty(this);
}
}
export class IfBlock implements Node {
export class IfBlock extends BlockNode implements Node {
constructor(
public branches: IfBlockBranch[], public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
public branches: IfBlockBranch[], sourceSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan|null,
nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitIfBlock(this);
}
}
export class IfBlockBranch implements Node {
export class IfBlockBranch extends BlockNode implements Node {
constructor(
public expression: AST|null, public children: Node[], public expressionAlias: Variable|null,
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null) {}
sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan|null, nameSpan: ParseSourceSpan) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitIfBlockBranch(this);

View file

@ -59,7 +59,8 @@ export function createIfBlock(
if (mainBlockParams !== null) {
branches.push(new t.IfBlockBranch(
mainBlockParams.expression, html.visitAll(visitor, ast.children, ast.children),
mainBlockParams.expressionAlias, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan));
mainBlockParams.expressionAlias, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan,
ast.nameSpan));
}
for (const block of connectedBlocks) {
@ -70,12 +71,13 @@ export function createIfBlock(
const children = html.visitAll(visitor, block.children, block.children);
branches.push(new t.IfBlockBranch(
params.expression, children, params.expressionAlias, block.sourceSpan,
block.startSourceSpan, block.endSourceSpan));
block.startSourceSpan, block.endSourceSpan, block.nameSpan));
}
} else if (block.name === 'else') {
const children = html.visitAll(visitor, block.children, block.children);
branches.push(new t.IfBlockBranch(
null, children, null, block.sourceSpan, block.startSourceSpan, block.endSourceSpan));
null, children, null, block.sourceSpan, block.startSourceSpan, block.endSourceSpan,
block.nameSpan));
}
}
@ -92,7 +94,8 @@ export function createIfBlock(
}
return {
node: new t.IfBlock(branches, wholeSourceSpan, ast.startSourceSpan, ifBlockEndSourceSpan),
node: new t.IfBlock(
branches, wholeSourceSpan, ast.startSourceSpan, ifBlockEndSourceSpan, ast.nameSpan),
errors,
};
}
@ -115,7 +118,7 @@ export function createForLoop(
} else {
empty = new t.ForLoopBlockEmpty(
html.visitAll(visitor, block.children, block.children), block.sourceSpan,
block.startSourceSpan, block.endSourceSpan);
block.startSourceSpan, block.endSourceSpan, block.nameSpan);
}
} else {
errors.push(new ParseError(block.sourceSpan, `Unrecognized @for loop block "${block.name}"`));
@ -135,9 +138,9 @@ export function createForLoop(
const sourceSpan =
new ParseSourceSpan(ast.sourceSpan.start, endSpan?.end ?? ast.sourceSpan.end);
node = new t.ForLoopBlock(
params.itemName, params.expression, params.trackBy, params.context,
html.visitAll(visitor, ast.children, ast.children), empty, sourceSpan, ast.sourceSpan,
ast.startSourceSpan, endSpan);
params.itemName, params.expression, params.trackBy.expression, params.trackBy.keywordSpan,
params.context, html.visitAll(visitor, ast.children, ast.children), empty, sourceSpan,
ast.sourceSpan, ast.startSourceSpan, endSpan, ast.nameSpan);
}
}
@ -172,7 +175,7 @@ export function createSwitchBlock(
null;
const ast = new t.SwitchBlockCase(
expression, html.visitAll(visitor, node.children, node.children), node.sourceSpan,
node.startSourceSpan, node.endSourceSpan);
node.startSourceSpan, node.endSourceSpan, node.nameSpan);
if (expression === null) {
defaultCase = ast;
@ -189,7 +192,7 @@ export function createSwitchBlock(
return {
node: new t.SwitchBlock(
primaryExpression, cases, unknownBlocks, ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan),
ast.endSourceSpan, ast.nameSpan),
errors
};
}
@ -217,7 +220,7 @@ function parseForLoopParameters(
const result = {
itemName: new t.Variable(
itemName, '$implicit', expressionParam.sourceSpan, expressionParam.sourceSpan),
trackBy: null as ASTWithSource | null,
trackBy: null as {expression: ASTWithSource, keywordSpan: ParseSourceSpan} | null,
expression: parseBlockParameterToBinding(expressionParam, bindingParser, rawExpression),
context: {} as t.ForLoopBlockContext,
};
@ -237,7 +240,10 @@ function parseForLoopParameters(
errors.push(
new ParseError(param.sourceSpan, '@for loop can only have one "track" expression'));
} else {
result.trackBy = parseBlockParameterToBinding(param, bindingParser, trackMatch[1]);
const expression = parseBlockParameterToBinding(param, bindingParser, trackMatch[1]);
const keywordSpan = new ParseSourceSpan(
param.sourceSpan.start, param.sourceSpan.start.moveBy('track'.length));
result.trackBy = {expression, keywordSpan};
}
continue;
}

View file

@ -48,8 +48,7 @@ export function createDeferredBlock(
const {triggers, prefetchTriggers} =
parsePrimaryTriggers(ast.parameters, bindingParser, errors, placeholder);
// The `defer` block has a main span encompassing all of the connected branches as well. For the
// span of only the first "main" branch, use `mainSourceSpan`.
// The `defer` block has a main span encompassing all of the connected branches as well.
let lastEndSourceSpan = ast.endSourceSpan;
let endOfLastSourceSpan = ast.sourceSpan.end;
if (connectedBlocks.length > 0) {
@ -58,12 +57,13 @@ export function createDeferredBlock(
endOfLastSourceSpan = lastConnectedBlock.sourceSpan.end;
}
const mainDeferredSourceSpan = new ParseSourceSpan(ast.sourceSpan.start, endOfLastSourceSpan);
const sourceSpanWithConnectedBlocks =
new ParseSourceSpan(ast.sourceSpan.start, endOfLastSourceSpan);
const node = new t.DeferredBlock(
html.visitAll(visitor, ast.children, ast.children), triggers, prefetchTriggers, placeholder,
loading, error, mainDeferredSourceSpan, ast.sourceSpan, ast.startSourceSpan,
lastEndSourceSpan);
loading, error, ast.nameSpan, sourceSpanWithConnectedBlocks, ast.sourceSpan,
ast.startSourceSpan, lastEndSourceSpan);
return {node, errors};
}
@ -140,7 +140,7 @@ function parsePlaceholderBlock(ast: html.Block, visitor: html.Visitor): t.Deferr
}
return new t.DeferredBlockPlaceholder(
html.visitAll(visitor, ast.children, ast.children), minimumTime, ast.sourceSpan,
html.visitAll(visitor, ast.children, ast.children), minimumTime, ast.nameSpan, ast.sourceSpan,
ast.startSourceSpan, ast.endSourceSpan);
}
@ -181,8 +181,8 @@ function parseLoadingBlock(ast: html.Block, visitor: html.Visitor): t.DeferredBl
}
return new t.DeferredBlockLoading(
html.visitAll(visitor, ast.children, ast.children), afterTime, minimumTime, ast.sourceSpan,
ast.startSourceSpan, ast.endSourceSpan);
html.visitAll(visitor, ast.children, ast.children), afterTime, minimumTime, ast.nameSpan,
ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan);
}
@ -192,8 +192,8 @@ function parseErrorBlock(ast: html.Block, visitor: html.Visitor): t.DeferredBloc
}
return new t.DeferredBlockError(
html.visitAll(visitor, ast.children, ast.children), ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan);
html.visitAll(visitor, ast.children, ast.children), ast.nameSpan, ast.sourceSpan,
ast.startSourceSpan, ast.endSourceSpan);
}
function parsePrimaryTriggers(

View file

@ -42,6 +42,9 @@ export function parseWhenTrigger(
{expression, sourceSpan}: html.BlockParameter, bindingParser: BindingParser,
triggers: t.DeferredBlockTriggers, errors: ParseError[]): void {
const whenIndex = expression.indexOf('when');
const whenSourceSpan = new ParseSourceSpan(
sourceSpan.start.moveBy(whenIndex), sourceSpan.start.moveBy(whenIndex + 'when'.length));
const prefetchSpan = getPrefetchSpan(expression, sourceSpan);
// This is here just to be safe, we shouldn't enter this function
// in the first place if a block doesn't have the "when" keyword.
@ -51,7 +54,9 @@ export function parseWhenTrigger(
const start = getTriggerParametersStart(expression, whenIndex + 1);
const parsed = bindingParser.parseBinding(
expression.slice(start), false, sourceSpan, sourceSpan.start.offset + start);
trackTrigger('when', triggers, errors, new t.BoundDeferredTrigger(parsed, sourceSpan));
trackTrigger(
'when', triggers, errors,
new t.BoundDeferredTrigger(parsed, sourceSpan, prefetchSpan, whenSourceSpan));
}
}
@ -60,6 +65,9 @@ export function parseOnTrigger(
{expression, sourceSpan}: html.BlockParameter, triggers: t.DeferredBlockTriggers,
errors: ParseError[], placeholder: t.DeferredBlockPlaceholder|null): void {
const onIndex = expression.indexOf('on');
const onSourceSpan = new ParseSourceSpan(
sourceSpan.start.moveBy(onIndex), sourceSpan.start.moveBy(onIndex + 'on'.length));
const prefetchSpan = getPrefetchSpan(expression, sourceSpan);
// This is here just to be safe, we shouldn't enter this function
// in the first place if a block doesn't have the "on" keyword.
@ -67,12 +75,19 @@ export function parseOnTrigger(
errors.push(new ParseError(sourceSpan, `Could not find "on" keyword in expression`));
} else {
const start = getTriggerParametersStart(expression, onIndex + 1);
const parser =
new OnTriggerParser(expression, start, sourceSpan, triggers, errors, placeholder);
const parser = new OnTriggerParser(
expression, start, sourceSpan, triggers, errors, placeholder, prefetchSpan, onSourceSpan);
parser.parse();
}
}
function getPrefetchSpan(expression: string, sourceSpan: ParseSourceSpan) {
if (!expression.startsWith('prefetch')) {
return null;
}
return new ParseSourceSpan(sourceSpan.start, sourceSpan.start.moveBy('prefetch'.length));
}
class OnTriggerParser {
private index = 0;
@ -81,7 +96,8 @@ class OnTriggerParser {
constructor(
private expression: string, private start: number, private span: ParseSourceSpan,
private triggers: t.DeferredBlockTriggers, private errors: ParseError[],
private placeholder: t.DeferredBlockPlaceholder|null) {
private placeholder: t.DeferredBlockPlaceholder|null,
private prefetchSpan: ParseSourceSpan|null, private onSourceSpan: ParseSourceSpan) {
this.tokens = new Lexer().tokenize(expression.slice(start));
}
@ -133,36 +149,66 @@ class OnTriggerParser {
}
private consumeTrigger(identifier: Token, parameters: string[]) {
const startSpan = this.span.start.moveBy(this.start + identifier.index - this.tokens[0].index);
const endSpan = startSpan.moveBy(this.token().end - identifier.index);
const sourceSpan = new ParseSourceSpan(startSpan, endSpan);
const triggerNameStartSpan =
this.span.start.moveBy(this.start + identifier.index - this.tokens[0].index);
const nameSpan = new ParseSourceSpan(
triggerNameStartSpan, triggerNameStartSpan.moveBy(identifier.strValue.length));
const endSpan = triggerNameStartSpan.moveBy(this.token().end - identifier.index);
// Put the prefetch and on spans with the first trigger
// This should maybe be refactored to have something like an outer OnGroup AST
// Since triggers can be grouped with commas "on hover(x), interaction(y)"
const isFirstTrigger = identifier.index === 0;
const onSourceSpan = isFirstTrigger ? this.onSourceSpan : null;
const prefetchSourceSpan = isFirstTrigger ? this.prefetchSpan : null;
const sourceSpan =
new ParseSourceSpan(isFirstTrigger ? this.span.start : triggerNameStartSpan, endSpan);
try {
switch (identifier.toString()) {
case OnTriggerType.IDLE:
this.trackTrigger('idle', createIdleTrigger(parameters, sourceSpan));
this.trackTrigger(
'idle',
createIdleTrigger(
parameters, nameSpan, sourceSpan, prefetchSourceSpan, onSourceSpan));
break;
case OnTriggerType.TIMER:
this.trackTrigger('timer', createTimerTrigger(parameters, sourceSpan));
this.trackTrigger(
'timer',
createTimerTrigger(
parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan));
break;
case OnTriggerType.INTERACTION:
this.trackTrigger(
'interaction', createInteractionTrigger(parameters, sourceSpan, this.placeholder));
'interaction',
createInteractionTrigger(
parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan,
this.placeholder));
break;
case OnTriggerType.IMMEDIATE:
this.trackTrigger('immediate', createImmediateTrigger(parameters, sourceSpan));
this.trackTrigger(
'immediate',
createImmediateTrigger(
parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan));
break;
case OnTriggerType.HOVER:
this.trackTrigger('hover', createHoverTrigger(parameters, sourceSpan, this.placeholder));
this.trackTrigger(
'hover',
createHoverTrigger(
parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan,
this.placeholder));
break;
case OnTriggerType.VIEWPORT:
this.trackTrigger(
'viewport', createViewportTrigger(parameters, sourceSpan, this.placeholder));
'viewport',
createViewportTrigger(
parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan,
this.placeholder));
break;
default:
@ -273,15 +319,26 @@ function trackTrigger(
}
function createIdleTrigger(
parameters: string[], sourceSpan: ParseSourceSpan): t.IdleDeferredTrigger {
parameters: string[],
nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null,
onSourceSpan: ParseSourceSpan|null,
): t.IdleDeferredTrigger {
if (parameters.length > 0) {
throw new Error(`"${OnTriggerType.IDLE}" trigger cannot have parameters`);
}
return new t.IdleDeferredTrigger(sourceSpan);
return new t.IdleDeferredTrigger(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function createTimerTrigger(parameters: string[], sourceSpan: ParseSourceSpan) {
function createTimerTrigger(
parameters: string[],
nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null,
onSourceSpan: ParseSourceSpan|null,
) {
if (parameters.length !== 1) {
throw new Error(`"${OnTriggerType.TIMER}" trigger must have exactly one parameter`);
}
@ -292,37 +349,48 @@ function createTimerTrigger(parameters: string[], sourceSpan: ParseSourceSpan) {
throw new Error(`Could not parse time value of trigger "${OnTriggerType.TIMER}"`);
}
return new t.TimerDeferredTrigger(delay, sourceSpan);
return new t.TimerDeferredTrigger(delay, nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function createImmediateTrigger(
parameters: string[], sourceSpan: ParseSourceSpan): t.ImmediateDeferredTrigger {
parameters: string[],
nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null,
onSourceSpan: ParseSourceSpan|null,
): t.ImmediateDeferredTrigger {
if (parameters.length > 0) {
throw new Error(`"${OnTriggerType.IMMEDIATE}" trigger cannot have parameters`);
}
return new t.ImmediateDeferredTrigger(sourceSpan);
return new t.ImmediateDeferredTrigger(nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function createHoverTrigger(
parameters: string[], sourceSpan: ParseSourceSpan,
parameters: string[], nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null,
placeholder: t.DeferredBlockPlaceholder|null): t.HoverDeferredTrigger {
validateReferenceBasedTrigger(OnTriggerType.HOVER, parameters, placeholder);
return new t.HoverDeferredTrigger(parameters[0] ?? null, sourceSpan);
return new t.HoverDeferredTrigger(
parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function createInteractionTrigger(
parameters: string[], sourceSpan: ParseSourceSpan,
parameters: string[], nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null,
placeholder: t.DeferredBlockPlaceholder|null): t.InteractionDeferredTrigger {
validateReferenceBasedTrigger(OnTriggerType.INTERACTION, parameters, placeholder);
return new t.InteractionDeferredTrigger(parameters[0] ?? null, sourceSpan);
return new t.InteractionDeferredTrigger(
parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function createViewportTrigger(
parameters: string[], sourceSpan: ParseSourceSpan,
parameters: string[], nameSpan: ParseSourceSpan, sourceSpan: ParseSourceSpan,
prefetchSpan: ParseSourceSpan|null, onSourceSpan: ParseSourceSpan|null,
placeholder: t.DeferredBlockPlaceholder|null): t.ViewportDeferredTrigger {
validateReferenceBasedTrigger(OnTriggerType.VIEWPORT, parameters, placeholder);
return new t.ViewportDeferredTrigger(parameters[0] ?? null, sourceSpan);
return new t.ViewportDeferredTrigger(
parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan);
}
function validateReferenceBasedTrigger(

View file

@ -213,5 +213,5 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
* @param block Block that the trigger belongs to.
* @param trigger Trigger whose target is being looked up.
*/
getDeferredTriggerTarget(block: DeferredTrigger, trigger: DeferredTrigger): Element|null;
getDeferredTriggerTarget(block: DeferredBlock, trigger: DeferredTrigger): Element|null;
}

View file

@ -628,13 +628,13 @@ describe('R3 AST source spans', () => {
'}'
],
['BoundDeferredTrigger', 'when isVisible() && foo'],
['HoverDeferredTrigger', 'hover(button)'],
['HoverDeferredTrigger', 'on hover(button)'],
['TimerDeferredTrigger', 'timer(10s)'],
['IdleDeferredTrigger', 'idle'],
['ImmediateDeferredTrigger', 'immediate'],
['InteractionDeferredTrigger', 'interaction(button)'],
['ViewportDeferredTrigger', 'viewport(container)'],
['ImmediateDeferredTrigger', 'immediate'],
['ImmediateDeferredTrigger', 'prefetch on immediate'],
['BoundDeferredTrigger', 'prefetch when isDataLoaded()'],
[
'Element', '<calendar-cmp [date]="current"/>', '<calendar-cmp [date]="current"/>',

View file

@ -25,6 +25,7 @@ export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.tex
export enum DisplayInfoKind {
ATTRIBUTE = 'attribute',
BLOCK = 'block',
TRIGGER = 'trigger',
COMPONENT = 'component',
DIRECTIVE = 'directive',
EVENT = 'event',
@ -35,6 +36,7 @@ export enum DisplayInfoKind {
PROPERTY = 'property',
METHOD = 'method',
TEMPLATE = 'template',
KEYWORD = 'keyword',
}
export interface DisplayInfo {

View file

@ -165,8 +165,7 @@ export class LanguageService {
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] :
positionDetails.context.node;
return new QuickInfoBuilder(
this.tsLS, compiler, templateInfo.component, node, positionDetails.parent)
return new QuickInfoBuilder(this.tsLS, compiler, templateInfo.component, node, positionDetails)
.get();
}

View file

@ -48,9 +48,7 @@ export function getOutliningSpans(compiler: NgCompiler, fileName: string): ts.Ou
}
class BlockVisitor extends t.RecursiveVisitor {
readonly blocks = [] as
Array<t.IfBlockBranch|t.ForLoopBlockEmpty|t.ForLoopBlock|t.SwitchBlockCase|t.SwitchBlock|
t.DeferredBlockError|t.DeferredBlockPlaceholder|t.DeferredBlockLoading>;
readonly blocks = [] as Array<t.BlockNode>;
static getBlockSpans(templateNodes: t.Node[]): ts.OutliningSpan[] {
const visitor = new BlockVisitor();
@ -78,11 +76,9 @@ class BlockVisitor extends t.RecursiveVisitor {
}
visit(node: t.Node) {
if (node instanceof t.IfBlockBranch || node instanceof t.ForLoopBlockEmpty ||
node instanceof t.ForLoopBlock || node instanceof t.SwitchBlockCase ||
node instanceof t.SwitchBlock || node instanceof t.DeferredBlockError ||
node instanceof t.DeferredBlockPlaceholder || node instanceof t.DeferredBlockLoading ||
node instanceof t.DeferredBlock) {
if (node instanceof t.BlockNode
// Omit `IfBlock` because we include the branches individually
&& !(node instanceof t.IfBlock)) {
this.blocks.push(node);
}
}

View file

@ -5,23 +5,31 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AST, Call, ImplicitReceiver, PropertyRead, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
import {AST, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, Symbol, SymbolKind, TcbLocation, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import {BlockNode, DeferredTrigger} from '@angular/compiler/src/render3/r3_ast';
import ts from 'typescript';
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTextSpanOfNode} from './utils';
import {DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT} from './display_parts';
import {createDollarAnyQuickInfo, createNgTemplateQuickInfo, createQuickInfoForBuiltIn, isDollarAny} from './quick_info_built_ins';
import {TemplateTarget} from './template_target';
import {createQuickInfo, filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTextSpanOfNode} from './utils';
export class QuickInfoBuilder {
private readonly typeChecker = this.compiler.getCurrentProgram().getTypeChecker();
private readonly parent = this.positionDetails.parent;
constructor(
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
private readonly component: ts.ClassDeclaration, private node: TmplAstNode|AST,
private parent: TmplAstNode|AST|null) {}
private readonly positionDetails: TemplateTarget) {}
get(): ts.QuickInfo|undefined {
if (this.node instanceof DeferredTrigger || this.node instanceof BlockNode) {
return createQuickInfoForBuiltIn(this.node, this.positionDetails.position);
}
const symbol =
this.compiler.getTemplateTypeChecker().getSymbolOfNode(this.node, this.component);
if (symbol !== null) {
@ -194,63 +202,3 @@ function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: DisplayInfoKind): ts
function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, kind: string}) {
return a.text === b.text && a.kind === b.kind;
}
function isDollarAny(node: TmplAstNode|AST): node is Call {
return node instanceof Call && node.receiver instanceof PropertyRead &&
node.receiver.receiver instanceof ImplicitReceiver &&
!(node.receiver.receiver instanceof ThisReceiver) && node.receiver.name === '$any' &&
node.args.length === 1;
}
function createDollarAnyQuickInfo(node: Call): ts.QuickInfo {
return createQuickInfo(
'$any',
DisplayInfoKind.METHOD,
getTextSpanOfNode(node.receiver),
/** containerName */ undefined,
'any',
[{
kind: SYMBOL_TEXT,
text: 'function to cast an expression to the `any` type',
}],
);
}
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
return createQuickInfo(
'ng-template',
DisplayInfoKind.TEMPLATE,
getTextSpanOfNode(node),
/** containerName */ undefined,
/** type */ undefined,
[{
kind: SYMBOL_TEXT,
text:
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
}],
);
}
/**
* Construct a QuickInfo object taking into account its container and type.
* @param name Name of the QuickInfo target
* @param kind component, directive, pipe, etc.
* @param textSpan span of the target
* @param containerName either the Symbol's container or the NgModule that contains the directive
* @param type user-friendly name of the type
* @param documentation docstring or comment
*/
export function createQuickInfo(
name: string, kind: DisplayInfoKind, textSpan: ts.TextSpan, containerName?: string,
type?: string, documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
const displayParts = createDisplayParts(name, kind, containerName, type);
return {
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: textSpan,
displayParts,
documentation,
};
}

View file

@ -0,0 +1,182 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AST, Call, ImplicitReceiver, ParseSourceSpan, PropertyRead, ThisReceiver, TmplAstDeferredBlock, TmplAstDeferredBlockError, TmplAstDeferredBlockLoading, TmplAstDeferredBlockPlaceholder, TmplAstNode} from '@angular/compiler';
import {BlockNode, DeferredTrigger, ForLoopBlock, ForLoopBlockEmpty} from '@angular/compiler/src/render3/r3_ast';
import ts from 'typescript';
import {DisplayInfoKind, SYMBOL_TEXT} from './display_parts';
import {createQuickInfo, getTextSpanOfNode, isWithin, toTextSpan} from './utils';
export function isDollarAny(node: TmplAstNode|AST): node is Call {
return node instanceof Call && node.receiver instanceof PropertyRead &&
node.receiver.receiver instanceof ImplicitReceiver &&
!(node.receiver.receiver instanceof ThisReceiver) && node.receiver.name === '$any' &&
node.args.length === 1;
}
export function createDollarAnyQuickInfo(node: Call): ts.QuickInfo {
return createQuickInfo(
'$any',
DisplayInfoKind.METHOD,
getTextSpanOfNode(node.receiver),
/** containerName */ undefined,
'any',
[{
kind: SYMBOL_TEXT,
text: 'function to cast an expression to the `any` type',
}],
);
}
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
export function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
return createQuickInfo(
'ng-template',
DisplayInfoKind.TEMPLATE,
getTextSpanOfNode(node),
/** containerName */ undefined,
/** type */ undefined,
[{
kind: SYMBOL_TEXT,
text:
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
}],
);
}
export function createQuickInfoForBuiltIn(
node: DeferredTrigger|BlockNode, cursorPositionInTemplate: number): ts.QuickInfo|undefined {
let partSpan: ParseSourceSpan;
if (node instanceof DeferredTrigger) {
if (node.prefetchSpan !== null && isWithin(cursorPositionInTemplate, node.prefetchSpan)) {
partSpan = node.prefetchSpan;
} else if (
node.whenOrOnSourceSpan !== null &&
isWithin(cursorPositionInTemplate, node.whenOrOnSourceSpan)) {
partSpan = node.whenOrOnSourceSpan;
} else if (node.nameSpan !== null && isWithin(cursorPositionInTemplate, node.nameSpan)) {
partSpan = node.nameSpan;
} else {
return undefined;
}
} else {
if (node instanceof TmplAstDeferredBlock || node instanceof TmplAstDeferredBlockError ||
node instanceof TmplAstDeferredBlockLoading ||
node instanceof TmplAstDeferredBlockPlaceholder ||
node instanceof ForLoopBlockEmpty && isWithin(cursorPositionInTemplate, node.nameSpan)) {
partSpan = node.nameSpan;
} else if (
node instanceof ForLoopBlock && isWithin(cursorPositionInTemplate, node.trackKeywordSpan)) {
partSpan = node.trackKeywordSpan;
} else {
return undefined;
}
}
const partName = partSpan.toString().trim();
const partInfo = BUILT_IN_NAMES_TO_DOC_MAP[partName];
const linkTags: ts.JSDocTagInfo[] =
(partInfo?.links ?? []).map(text => ({text: [{kind: SYMBOL_TEXT, text}], name: 'see'}));
return createQuickInfo(
partName,
partInfo.displayInfoKind,
toTextSpan(partSpan),
/** containerName */ undefined,
/** type */ undefined,
[{
kind: SYMBOL_TEXT,
text: partInfo?.docString ?? '',
}],
linkTags,
);
}
const triggerDescriptionPreamble = 'A trigger to start loading the defer content after ';
const BUILT_IN_NAMES_TO_DOC_MAP: {
[name: string]: {docString: string, links: string[], displayInfoKind: DisplayInfoKind}
} = {
'@defer': {
docString:
`A type of block that can be used to defer load the JavaScript for components, directives and pipes used inside a component template.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.BLOCK,
},
'@placeholder': {
docString: `A block for content shown prior to defer loading (Optional)`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.BLOCK,
},
'@error': {
docString: `A block for content shown when defer loading errors occur (Optional)`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.BLOCK,
},
'@loading': {
docString: `A block for content shown during defer loading (Optional)`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.BLOCK,
},
'@empty': {
docString: `A block to display when the for loop variable is empty.`,
links: ['[AIO Reference](https://next.angular.io/api/core/for)'],
displayInfoKind: DisplayInfoKind.BLOCK,
},
'track': {
docString: `Keyword to control how the for loop compares items in the list to compute updates.`,
links: ['[AIO Reference](https://next.angular.io/api/core/for)'],
displayInfoKind: DisplayInfoKind.KEYWORD,
},
'idle': {
docString: triggerDescriptionPreamble + `the browser reports idle state (default).`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'immediate': {
docString: triggerDescriptionPreamble + `the page finishes rendering.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'hover': {
docString: triggerDescriptionPreamble + `the element has been hovered.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'timer': {
docString: triggerDescriptionPreamble + `a specific timeout.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'interaction': {
docString: triggerDescriptionPreamble + `the element is clicked, touched, or focused.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'viewport': {
docString: triggerDescriptionPreamble + `the element enters the viewport.`,
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.TRIGGER,
},
'prefetch': {
docString:
'Keyword that indicates that the trigger configures when prefetching the defer block contents should start. You can use `on` and `when` conditions as prefetch triggers.',
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.KEYWORD,
},
'when': {
docString:
'Keyword that starts the expression-based trigger section. Should be followed by an expression that returns a boolean.',
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.KEYWORD,
},
'on': {
docString:
'Keyword that starts the event-based trigger section. Should be followed by one of the built-in triggers.',
links: ['[AIO Reference](https://next.angular.io/api/core/defer)'],
displayInfoKind: DisplayInfoKind.KEYWORD,
},
};

View file

@ -15,7 +15,7 @@ import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expr
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
import ts from 'typescript';
import {ALIAS_NAME, SYMBOL_PUNC} from './display_parts';
import {ALIAS_NAME, createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
import {findTightestNode, getParentClassDeclaration} from './ts_utils';
export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
@ -402,3 +402,27 @@ export function isBoundEventWithSyntheticHandler(event: t.BoundEvent): boolean {
}
return false;
}
/**
* Construct a QuickInfo object taking into account its container and type.
* @param name Name of the QuickInfo target
* @param kind component, directive, pipe, etc.
* @param textSpan span of the target
* @param containerName either the Symbol's container or the NgModule that contains the directive
* @param type user-friendly name of the type
* @param documentation docstring or comment
*/
export function createQuickInfo(
name: string, kind: DisplayInfoKind, textSpan: ts.TextSpan, containerName?: string,
type?: string, documentation?: ts.SymbolDisplayPart[], tags?: ts.JSDocTagInfo[]): ts.QuickInfo {
const displayParts = createDisplayParts(name, kind, containerName, type);
return {
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: textSpan,
displayParts,
documentation,
tags,
};
}

View file

@ -522,6 +522,148 @@ describe('quick info', () => {
});
});
describe('blocks', () => {
describe('defer & friends', () => {
it('defer', () => {
expectQuickInfo({
templateOverride: `@de¦fer { } @placeholder { <input /> }`,
expectedSpanText: '@defer ',
expectedDisplayString: '(block) @defer'
});
});
it('defer with condition', () => {
expectQuickInfo({
templateOverride: `@de¦fer (on immediate) { } @placeholder { <input /> }`,
expectedSpanText: '@defer ',
expectedDisplayString: '(block) @defer'
});
});
it('placeholder', () => {
expectQuickInfo({
templateOverride: `@defer { } @pla¦ceholder { <input /> }`,
expectedSpanText: '@placeholder ',
expectedDisplayString: '(block) @placeholder'
});
});
it('loading', () => {
expectQuickInfo({
templateOverride: `@defer { } @loadin¦g { <input /> }`,
expectedSpanText: '@loading ',
expectedDisplayString: '(block) @loading'
});
});
it('error', () => {
expectQuickInfo({
templateOverride: `@defer { } @erro¦r { <input /> }`,
expectedSpanText: '@error ',
expectedDisplayString: '(block) @error'
});
});
describe('triggers', () => {
it('viewport', () => {
expectQuickInfo({
templateOverride: `@defer (on vie¦wport(x)) { } <div #x></div>`,
expectedSpanText: 'viewport',
expectedDisplayString: '(trigger) viewport'
});
});
it('immediate', () => {
expectQuickInfo({
templateOverride: `@defer (on imme¦diate) {}`,
expectedSpanText: 'immediate',
expectedDisplayString: '(trigger) immediate'
});
});
it('idle', () => {
expectQuickInfo({
templateOverride: `@defer (on i¦dle) { } `,
expectedSpanText: 'idle',
expectedDisplayString: '(trigger) idle'
});
});
it('hover', () => {
expectQuickInfo({
templateOverride: `@defer (on hov¦er(x)) { } <div #x></div> `,
expectedSpanText: 'hover',
expectedDisplayString: '(trigger) hover'
});
});
it('timer', () => {
expectQuickInfo({
templateOverride: `@defer (on tim¦er(100)) { } `,
expectedSpanText: 'timer',
expectedDisplayString: '(trigger) timer'
});
});
it('interaction', () => {
expectQuickInfo({
templateOverride: `@defer (on interactio¦n(x)) { } <div #x></div>`,
expectedSpanText: 'interaction',
expectedDisplayString: '(trigger) interaction'
});
});
it('when', () => {
expectQuickInfo({
templateOverride: `@defer (whe¦n title) { } <div #x></div>`,
expectedSpanText: 'when',
expectedDisplayString: '(keyword) when'
});
});
it('prefetch (when)', () => {
expectQuickInfo({
templateOverride: `@defer (prefet¦ch when title) { }`,
expectedSpanText: 'prefetch',
expectedDisplayString: '(keyword) prefetch'
});
});
it('on', () => {
expectQuickInfo({
templateOverride: `@defer (o¦n immediate) { } `,
expectedSpanText: 'on',
expectedDisplayString: '(keyword) on'
});
});
it('prefetch (on)', () => {
expectQuickInfo({
templateOverride: `@defer (prefet¦ch on immediate) { }`,
expectedSpanText: 'prefetch',
expectedDisplayString: '(keyword) prefetch'
});
});
});
});
it('empty', () => {
expectQuickInfo({
templateOverride: `@for (name of constNames; track $index) {} @em¦pty {}`,
expectedSpanText: '@empty ',
expectedDisplayString: '(block) @empty'
});
});
it('track keyword', () => {
expectQuickInfo({
templateOverride: `@for (name of constNames; tr¦ack $index) {}`,
expectedSpanText: 'track',
expectedDisplayString: '(keyword) track'
});
});
});
it('should work for object literal with shorthand property declarations', () => {
initMockFileSystem('Native');
env = LanguageServiceTestEnv.setup();