diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index 6fe2c2346dd..ff92022f0a8 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): 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; readonly prefetchTriggers: Readonly; 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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): 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(visitor: Visitor): Result { return visitor.visitIfBlockBranch(this); diff --git a/packages/compiler/src/render3/r3_control_flow.ts b/packages/compiler/src/render3/r3_control_flow.ts index b5960f15baf..81aa3f3f693 100644 --- a/packages/compiler/src/render3/r3_control_flow.ts +++ b/packages/compiler/src/render3/r3_control_flow.ts @@ -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; } diff --git a/packages/compiler/src/render3/r3_deferred_blocks.ts b/packages/compiler/src/render3/r3_deferred_blocks.ts index 9f6637941c0..5ef7548d4c3 100644 --- a/packages/compiler/src/render3/r3_deferred_blocks.ts +++ b/packages/compiler/src/render3/r3_deferred_blocks.ts @@ -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( diff --git a/packages/compiler/src/render3/r3_deferred_triggers.ts b/packages/compiler/src/render3/r3_deferred_triggers.ts index c89a0a55fde..fb28325ccb8 100644 --- a/packages/compiler/src/render3/r3_deferred_triggers.ts +++ b/packages/compiler/src/render3/r3_deferred_triggers.ts @@ -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( diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index 8179595cd34..f4e9662948d 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -213,5 +213,5 @@ export interface BoundTarget { * @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; } diff --git a/packages/compiler/test/render3/r3_ast_spans_spec.ts b/packages/compiler/test/render3/r3_ast_spans_spec.ts index 92cb8e9b43e..33b7abb782d 100644 --- a/packages/compiler/test/render3/r3_ast_spans_spec.ts +++ b/packages/compiler/test/render3/r3_ast_spans_spec.ts @@ -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', '', '', diff --git a/packages/language-service/src/display_parts.ts b/packages/language-service/src/display_parts.ts index 71d7288f7a3..15eef06f251 100644 --- a/packages/language-service/src/display_parts.ts +++ b/packages/language-service/src/display_parts.ts @@ -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 { diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index d04875ac19c..da636b31de3 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -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(); } diff --git a/packages/language-service/src/outlining_spans.ts b/packages/language-service/src/outlining_spans.ts index a85db263711..b1bfe6362c4 100644 --- a/packages/language-service/src/outlining_spans.ts +++ b/packages/language-service/src/outlining_spans.ts @@ -48,9 +48,7 @@ export function getOutliningSpans(compiler: NgCompiler, fileName: string): ts.Ou } class BlockVisitor extends t.RecursiveVisitor { - readonly blocks = [] as - Array; + readonly blocks = [] as Array; 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); } } diff --git a/packages/language-service/src/quick_info.ts b/packages/language-service/src/quick_info.ts index eb99e281346..8fd498c399a 100644 --- a/packages/language-service/src/quick_info.ts +++ b/packages/language-service/src/quick_info.ts @@ -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 `` 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, - }; -} diff --git a/packages/language-service/src/quick_info_built_ins.ts b/packages/language-service/src/quick_info_built_ins.ts new file mode 100644 index 00000000000..4179dee9a92 --- /dev/null +++ b/packages/language-service/src/quick_info_built_ins.ts @@ -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 `` 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, + }, +}; diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts index 99ed826f391..edd809fe5e0 100644 --- a/packages/language-service/src/utils.ts +++ b/packages/language-service/src/utils.ts @@ -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, + }; +} diff --git a/packages/language-service/test/quick_info_spec.ts b/packages/language-service/test/quick_info_spec.ts index b08110269f5..22575d6e294 100644 --- a/packages/language-service/test/quick_info_spec.ts +++ b/packages/language-service/test/quick_info_spec.ts @@ -522,6 +522,148 @@ describe('quick info', () => { }); }); + describe('blocks', () => { + describe('defer & friends', () => { + it('defer', () => { + expectQuickInfo({ + templateOverride: `@de¦fer { } @placeholder { }`, + expectedSpanText: '@defer ', + expectedDisplayString: '(block) @defer' + }); + }); + + it('defer with condition', () => { + expectQuickInfo({ + templateOverride: `@de¦fer (on immediate) { } @placeholder { }`, + expectedSpanText: '@defer ', + expectedDisplayString: '(block) @defer' + }); + }); + + it('placeholder', () => { + expectQuickInfo({ + templateOverride: `@defer { } @pla¦ceholder { }`, + expectedSpanText: '@placeholder ', + expectedDisplayString: '(block) @placeholder' + }); + }); + + it('loading', () => { + expectQuickInfo({ + templateOverride: `@defer { } @loadin¦g { }`, + expectedSpanText: '@loading ', + expectedDisplayString: '(block) @loading' + }); + }); + + it('error', () => { + expectQuickInfo({ + templateOverride: `@defer { } @erro¦r { }`, + expectedSpanText: '@error ', + expectedDisplayString: '(block) @error' + }); + }); + + describe('triggers', () => { + it('viewport', () => { + expectQuickInfo({ + templateOverride: `@defer (on vie¦wport(x)) { }
`, + 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)) { }
`, + 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)) { }
`, + expectedSpanText: 'interaction', + expectedDisplayString: '(trigger) interaction' + }); + }); + + it('when', () => { + expectQuickInfo({ + templateOverride: `@defer (whe¦n title) { }
`, + 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();