feat(compiler): Add sourceSpan and keySpan to TemplateBinding (#35897)
This commit adds fine-grained text spans to TemplateBinding for microsyntax expressions. 1. Source span By convention, source span refers to the entire span of the binding, including its key and value. 2. Key span Span of the binding key, without any whitespace or keywords like `let` The value span is captured by the value expression AST. This is part of a series of PRs to fix source span mapping in microsyntax expression. For more info, see the doc https://docs.google.com/document/d/1mEVF2pSSMSnOloqOPQTYNiAJO0XQxA1H0BZyESASOrE/edit?usp=sharing PR Close #35897
This commit is contained in:

committed by
Matias Niemelä

parent
32f099aa36
commit
06779cfe24
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ReferenceAst, TagContentType, TemplateBinding, Text, getHtmlTagDefinition} from '@angular/compiler';
|
||||
import {AST, ASTWithSource, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, HtmlAstPath, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, getHtmlTagDefinition} from '@angular/compiler';
|
||||
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||
|
||||
import {AstResult} from './common';
|
||||
@ -71,6 +71,8 @@ enum ATTR {
|
||||
// Group 10 = identifier inside ()
|
||||
IDENT_EVENT_IDX = 10,
|
||||
}
|
||||
// Microsyntax template starts with '*'. See https://angular.io/api/core/TemplateRef
|
||||
const TEMPLATE_ATTR_PREFIX = '*';
|
||||
|
||||
function isIdentifierPart(code: number) {
|
||||
// Identifiers consist of alphanumeric characters, '_', or '$'.
|
||||
@ -231,8 +233,7 @@ function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.Compl
|
||||
// bind parts for cases like [()|]
|
||||
// ^ cursor is here
|
||||
const bindParts = attr.name.match(BIND_NAME_REGEXP);
|
||||
// TemplateRef starts with '*'. See https://angular.io/api/core/TemplateRef
|
||||
const isTemplateRef = attr.name.startsWith('*');
|
||||
const isTemplateRef = attr.name.startsWith(TEMPLATE_ATTR_PREFIX);
|
||||
const isBinding = bindParts !== null || isTemplateRef;
|
||||
|
||||
if (!isBinding) {
|
||||
@ -450,15 +451,21 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
||||
}
|
||||
|
||||
visitAttr(ast: AttrAst) {
|
||||
if (ast.name.startsWith('*')) {
|
||||
if (ast.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
||||
// This a template binding given by micro syntax expression.
|
||||
// First, verify the attribute consists of some binding we can give completions for.
|
||||
// The sourceSpan of AttrAst points to the RHS of the attribute
|
||||
const templateKey = ast.name.substring(TEMPLATE_ATTR_PREFIX.length);
|
||||
const templateValue = ast.sourceSpan.toString();
|
||||
const templateUrl = ast.sourceSpan.start.file.url;
|
||||
// TODO(kyliau): We are unable to determine the absolute offset of the key
|
||||
// but it is okay here, because we are only looking at the RHS of the attr
|
||||
const absKeyOffset = 0;
|
||||
const absValueOffset = ast.sourceSpan.start.offset;
|
||||
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
||||
ast.name, ast.value, ast.sourceSpan.toString(), ast.sourceSpan.start.offset);
|
||||
// Find where the cursor is relative to the start of the attribute value.
|
||||
const valueRelativePosition = this.position - ast.sourceSpan.start.offset;
|
||||
templateKey, templateValue, templateUrl, absKeyOffset, absValueOffset);
|
||||
// Find the template binding that contains the position.
|
||||
const binding = templateBindings.find(b => inSpan(valueRelativePosition, b.span));
|
||||
const binding = templateBindings.find(b => inSpan(this.position, b.sourceSpan));
|
||||
|
||||
if (!binding) {
|
||||
return;
|
||||
@ -549,7 +556,10 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
||||
|
||||
const valueRelativePosition = this.position - attr.sourceSpan.start.offset;
|
||||
|
||||
if (binding.keyIsVar) {
|
||||
if (binding instanceof VariableBinding) {
|
||||
// TODO(kyliau): With expression sourceSpan we shouldn't have to search
|
||||
// the attribute value string anymore. Just check if position is in the
|
||||
// expression source span.
|
||||
const equalLocation = attr.value.indexOf('=');
|
||||
if (equalLocation > 0 && valueRelativePosition > equalLocation) {
|
||||
// We are after the '=' in a let clause. The valid values here are the members of the
|
||||
@ -566,9 +576,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (binding.value && inSpan(valueRelativePosition, binding.value.ast.span)) {
|
||||
this.processExpressionCompletions(binding.value.ast);
|
||||
else if (inSpan(valueRelativePosition, binding.value?.ast.span)) {
|
||||
this.processExpressionCompletions(binding.value !.ast);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, Attribute, BoundDirectivePropertyAst, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, RecursiveTemplateAstVisitor, SelectorMatcher, StaticSymbol, TemplateAst, TemplateAstPath, templateVisitAll, tokenReference} from '@angular/compiler';
|
||||
import {AST, Attribute, BoundDirectivePropertyAst, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, ExpressionBinding, RecursiveTemplateAstVisitor, SelectorMatcher, StaticSymbol, TemplateAst, TemplateAstPath, VariableBinding, templateVisitAll, tokenReference} from '@angular/compiler';
|
||||
import * as tss from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
import {AstResult} from './common';
|
||||
@ -200,33 +200,44 @@ function getSymbolInMicrosyntax(info: AstResult, path: TemplateAstPath, attribut
|
||||
if (!attribute.valueSpan) {
|
||||
return;
|
||||
}
|
||||
const absValueOffset = attribute.valueSpan.start.offset;
|
||||
let result: {symbol: Symbol, span: Span}|undefined;
|
||||
const {templateBindings} = info.expressionParser.parseTemplateBindings(
|
||||
attribute.name, attribute.value, attribute.sourceSpan.toString(),
|
||||
attribute.valueSpan.start.offset);
|
||||
// Find where the cursor is relative to the start of the attribute value.
|
||||
const valueRelativePosition = path.position - attribute.valueSpan.start.offset;
|
||||
attribute.sourceSpan.start.offset, attribute.valueSpan.start.offset);
|
||||
|
||||
// Find the symbol that contains the position.
|
||||
templateBindings.filter(tb => !tb.keyIsVar).forEach(tb => {
|
||||
if (inSpan(valueRelativePosition, tb.value?.ast.span)) {
|
||||
for (const tb of templateBindings) {
|
||||
if (tb instanceof VariableBinding) {
|
||||
// TODO(kyliau): if binding is variable we should still look for the value
|
||||
// of the key. For example, "let i=index" => "index" should point to
|
||||
// NgForOfContext.index
|
||||
continue;
|
||||
}
|
||||
if (inSpan(path.position, tb.value?.ast.sourceSpan)) {
|
||||
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
||||
const scope = getExpressionScope(dinfo, path);
|
||||
result = getExpressionSymbol(scope, tb.value !, path.position, info.template.query);
|
||||
} else if (inSpan(valueRelativePosition, tb.span)) {
|
||||
} else if (inSpan(path.position, tb.sourceSpan)) {
|
||||
const template = path.first(EmbeddedTemplateAst);
|
||||
if (template) {
|
||||
// One element can only have one template binding.
|
||||
const directiveAst = template.directives[0];
|
||||
if (directiveAst) {
|
||||
const symbol = findInputBinding(info, tb.key.substring(1), directiveAst);
|
||||
const symbol = findInputBinding(info, tb.key.source.substring(1), directiveAst);
|
||||
if (symbol) {
|
||||
result = {symbol, span: tb.span};
|
||||
result = {
|
||||
symbol,
|
||||
// the span here has to be relative to the start of the template
|
||||
// value so deduct the absolute offset.
|
||||
// TODO(kyliau): Use absolute source span throughout completions.
|
||||
span: offsetSpan(tb.key.span, -absValueOffset),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -310,9 +310,7 @@ describe('definitions', () => {
|
||||
});
|
||||
|
||||
it('should be able to find the directive property', () => {
|
||||
mockHost.override(
|
||||
TEST_TEMPLATE,
|
||||
`<div *ngFor="let item of heroes; ~{start-my}«trackBy»: test~{end-my};"></div>`);
|
||||
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let item of heroes; «trackBy»: test;"></div>`);
|
||||
|
||||
// Get the marker for trackBy in the code added above.
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'trackBy');
|
||||
@ -322,8 +320,7 @@ describe('definitions', () => {
|
||||
const {textSpan, definitions} = result !;
|
||||
|
||||
// Get the marker for bounded text in the code added above
|
||||
const boundedText = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'my');
|
||||
expect(textSpan).toEqual(boundedText);
|
||||
expect(textSpan).toEqual(marker);
|
||||
|
||||
expect(definitions).toBeDefined();
|
||||
// The two definitions are setter and getter of 'ngForTrackBy'.
|
||||
|
@ -118,9 +118,8 @@ describe('hover', () => {
|
||||
});
|
||||
|
||||
it('should work for structural directive inputs', () => {
|
||||
mockHost.override(
|
||||
TEST_TEMPLATE, `<div *ngFor="let item of heroes; «ᐱtrackByᐱ: test»;"></div>`);
|
||||
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'trackBy');
|
||||
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let item of heroes; «trackBy»: test;"></div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'trackBy');
|
||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo !;
|
||||
|
Reference in New Issue
Block a user