mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
73c5d1c04a
commit
bf5bda448f
13 changed files with 577 additions and 167 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"/>',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
182
packages/language-service/src/quick_info_built_ins.ts
Normal file
182
packages/language-service/src/quick_info_built_ins.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue