Skip to content

Commit

Permalink
refactor(language-service): add context to template target system (#4…
Browse files Browse the repository at this point in the history
…0032)

This commit extends the template targeting system, which determines the node
being referenced given a template position, to return additional context if
needed about the particular aspect of the node to which the position refers.
For example, a position pointing to an element node may be pointing either
to its tag name or to somewhere in the node body. This is the difference
between `<div|>` and `<div foo | bar>`.

PR Close #40032
  • Loading branch information
alxhub committed Dec 14, 2020
1 parent 524d581 commit ccaf48d
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 89 deletions.
11 changes: 7 additions & 4 deletions packages/language-service/ivy/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
private readonly component: ts.ClassDeclaration, private readonly node: N,
private readonly nodeParent: TmplAstNode|AST|null,
private readonly context: TmplAstTemplate|null) {}
private readonly template: TmplAstTemplate|null) {}

/**
* Analogue for `ts.LanguageService.getCompletionsAtPosition`.
Expand Down Expand Up @@ -185,7 +185,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
this: PropertyExpressionCompletionBuilder,
options: ts.GetCompletionsAtPositionOptions|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}
Expand Down Expand Up @@ -248,7 +249,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
this: PropertyExpressionCompletionBuilder, entryName: string,
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}
Expand Down Expand Up @@ -288,7 +290,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
*/
private getGlobalPropertyExpressionCompletionSymbol(
this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined {
const completions = this.templateTypeChecker.getGlobalCompletions(this.context, this.component);
const completions =
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
if (completions === null) {
return undefined;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/language-service/ivy/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,14 @@ export class DefinitionBuilder {
if (target === null) {
return undefined;
}
const {node, parent} = target;
const {nodeInContext, parent} = target;

const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
const symbol =
this.compiler.getTemplateTypeChecker().getSymbolOfNode(nodeInContext.node, component);
if (symbol === null) {
return undefined;
}
return {node, parent, symbol};
return {node: nodeInContext.node, parent, symbol};
}
}

Expand Down
7 changes: 4 additions & 3 deletions packages/language-service/ivy/language_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export class LanguageService {
return undefined;
}
const results =
new QuickInfoBuilder(this.tsLS, compiler, templateInfo.component, positionDetails.node)
new QuickInfoBuilder(
this.tsLS, compiler, templateInfo.component, positionDetails.nodeInContext.node)
.get();
this.compilerFactory.registerLastKnownProgram();
return results;
Expand All @@ -131,8 +132,8 @@ export class LanguageService {
return null;
}
return new CompletionBuilder(
this.tsLS, compiler, templateInfo.component, positionDetails.node, positionDetails.parent,
positionDetails.context);
this.tsLS, compiler, templateInfo.component, positionDetails.nodeInContext.node,
positionDetails.parent, positionDetails.template);
}

getCompletionsAtPosition(
Expand Down
11 changes: 6 additions & 5 deletions packages/language-service/ivy/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ export class ReferenceBuilder {
return undefined;
}

const node = positionDetails.nodeInContext.node;

// Get the information about the TCB at the template position.
const symbol = this.ttc.getSymbolOfNode(positionDetails.node, component);
const symbol = this.ttc.getSymbolOfNode(node, component);
if (symbol === null) {
return undefined;
}
Expand All @@ -56,12 +58,11 @@ export class ReferenceBuilder {
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
// have a shim location. This means we can't match dom bindings to their lib.dom reference,
// but we can still see if they match to a directive.
if (!(positionDetails.node instanceof TmplAstTextAttribute) &&
!(positionDetails.node instanceof TmplAstBoundAttribute)) {
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
return undefined;
}
const directives = getDirectiveMatchesForAttribute(
positionDetails.node.name, symbol.host.templateNode, symbol.host.directives);
node.name, symbol.host.templateNode, symbol.host.directives);
return this.getReferencesForDirectives(directives);
}
case SymbolKind.Reference: {
Expand All @@ -71,7 +72,7 @@ export class ReferenceBuilder {
case SymbolKind.Variable: {
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
const localVarPosition = symbol.localVarLocation.positionInShimFile;
const templateNode = positionDetails.node;
const templateNode = positionDetails.nodeInContext.node;

if ((templateNode instanceof TmplAstVariable)) {
if (templateNode.valueSpan !== undefined && isWithin(position, templateNode.valueSpan)) {
Expand Down
93 changes: 90 additions & 3 deletions packages/language-service/ivy/template_target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,74 @@ export interface TemplateTarget {
/**
* The template node (or AST expression) closest to the search position.
*/
node: t.Node|e.AST;
nodeInContext: TargetNode;

/**
* The `t.Template` which contains the found node or expression (or `null` if in the root
* template).
*/
context: t.Template|null;
template: t.Template|null;

/**
* The immediate parent node of the targeted node.
*/
parent: t.Node|e.AST|null;
}

/**
* A node targeted at a given position in the template, including potential contextual information
* about the specific aspect of the node being referenced.
*
* Some nodes have multiple interior contexts. For example, `t.Element` nodes have both a tag name
* as well as a body, and a given position definitively points to one or the other. `TargetNode`
* captures the node itself, as well as this additional contextual disambiguation.
*/
export type TargetNode = RawExpression|RawTemplateNode|ElementInBodyContext|ElementInTagContext;

/**
* Differentiates the various kinds of `TargetNode`s.
*/
export enum TargetNodeKind {
RawExpression,
RawTemplateNode,
ElementInTagContext,
ElementInBodyContext,
}

/**
* An `e.AST` expression that's targeted at a given position, with no additional context.
*/
export interface RawExpression {
kind: TargetNodeKind.RawExpression;
node: e.AST;
}

/**
* A `t.Node` template node that's targeted at a given position, with no additional context.
*/
export interface RawTemplateNode {
kind: TargetNodeKind.RawTemplateNode;
node: t.Node;
}

/**
* A `t.Element` (or `t.Template`) element node that's targeted, where the given position is within
* the tag name.
*/
export interface ElementInTagContext {
kind: TargetNodeKind.ElementInTagContext;
node: t.Element|t.Template;
}

/**
* A `t.Element` (or `t.Template`) element node that's targeted, where the given position is within
* the element body.
*/
export interface ElementInBodyContext {
kind: TargetNodeKind.ElementInBodyContext;
node: t.Element|t.Template;
}

/**
* Return the template AST node or expression AST node that most accurately
* represents the node at the specified cursor `position`.
Expand Down Expand Up @@ -77,7 +131,40 @@ export function getTargetAtPosition(template: t.Node[], position: number): Templ
parent = path[path.length - 2];
}

return {position, node: candidate, context, parent};
// Given the candidate node, determine the full targeted context.
let nodeInContext: TargetNode;
if (candidate instanceof e.AST) {
nodeInContext = {
kind: TargetNodeKind.RawExpression,
node: candidate,
};
} else if (candidate instanceof t.Element) {
// Elements have two contexts: the tag context (position is within the element tag) or the
// element body context (position is outside of the tag name, but still in the element).

// Calculate the end of the element tag name. Any position beyond this is in the element body.
const tagEndPos =
candidate.sourceSpan.start.offset + 1 /* '<' element open */ + candidate.name.length;
if (position > tagEndPos) {
// Position is within the element body
nodeInContext = {
kind: TargetNodeKind.ElementInBodyContext,
node: candidate,
};
} else {
nodeInContext = {
kind: TargetNodeKind.ElementInTagContext,
node: candidate,
};
}
} else {
nodeInContext = {
kind: TargetNodeKind.RawTemplateNode,
node: candidate,
};
}

return {position, nodeInContext, template: context, parent};
}

/**
Expand Down

0 comments on commit ccaf48d

Please sign in to comment.