fix(language-service): shorthand syntax with variables (#40239)
This commit fixes an issue in the ivy native language service that caused the logic that finds a target node given a template position to throw away the results. This happened because the source span of a variable node in the shorthand structural directive syntax (i.e. `*ngIf=`) included the entire binding. The result was that we would add the variable node to the path and then later detect that the cursor was outside the key and value spans and throw away the whole result. In general, we do this because we do not want to show information when the cursor is between a key/value (`inputA=¦"123"`). However, when using the shorthand syntax, we run into the situation where we can match an `AttributeBinding` as well as the vaariable in `*ngIf="som¦eValue as myLocalVar"`. This commit updates the visitor to retain enough information in the visit path to throw away invalid targets but keep valid ones if there were multiple results on a `t.Element` or `t.Template`. PR Close #40239
This commit is contained in:
parent
382f906948
commit
12cb39c1a4
|
@ -6,11 +6,11 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TmplAstBoundEvent} from '@angular/compiler';
|
import {ParseSpan, TmplAstBoundEvent} from '@angular/compiler';
|
||||||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||||
|
|
||||||
import {isTemplateNode, isTemplateNodeWithKeyAndValue, isWithin} from './utils';
|
import {isTemplateNode, isTemplateNodeWithKeyAndValue, isWithin, isWithinKeyValue} from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contextual information for a target position within the template.
|
* Contextual information for a target position within the template.
|
||||||
|
@ -105,6 +105,12 @@ export interface AttributeInValueContext {
|
||||||
node: t.TextAttribute|t.BoundAttribute|t.BoundEvent;
|
node: t.TextAttribute|t.BoundAttribute|t.BoundEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This special marker is added to the path when the cursor is within the sourceSpan but not the key
|
||||||
|
* or value span of a node with key/value spans.
|
||||||
|
*/
|
||||||
|
const OUTSIDE_K_V_MARKER = new e.AST(new ParseSpan(-1, -1), new e.AbsoluteSourceSpan(-1, -1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the template AST node or expression AST node that most accurately
|
* Return the template AST node or expression AST node that most accurately
|
||||||
* represents the node at the specified cursor `position`.
|
* represents the node at the specified cursor `position`.
|
||||||
|
@ -119,20 +125,6 @@ export function getTargetAtPosition(template: t.Node[], position: number): Templ
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidate = path[path.length - 1];
|
const candidate = path[path.length - 1];
|
||||||
if (isTemplateNodeWithKeyAndValue(candidate)) {
|
|
||||||
let {keySpan, valueSpan} = candidate;
|
|
||||||
if (valueSpan === undefined && candidate instanceof TmplAstBoundEvent) {
|
|
||||||
valueSpan = candidate.handlerSpan;
|
|
||||||
}
|
|
||||||
const isWithinKeyValue =
|
|
||||||
isWithin(position, keySpan) || (valueSpan && isWithin(position, valueSpan));
|
|
||||||
if (!isWithinKeyValue) {
|
|
||||||
// If cursor is within source span but not within key span or value span,
|
|
||||||
// do not return the node.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk up the result nodes to find the nearest `t.Template` which contains the targeted node.
|
// Walk up the result nodes to find the nearest `t.Template` which contains the targeted node.
|
||||||
let context: t.Template|null = null;
|
let context: t.Template|null = null;
|
||||||
for (let i = path.length - 2; i >= 0; i--) {
|
for (let i = path.length - 2; i >= 0; i--) {
|
||||||
|
@ -212,7 +204,22 @@ class TemplateTargetVisitor implements t.Visitor {
|
||||||
static visitTemplate(template: t.Node[], position: number): Array<t.Node|e.AST> {
|
static visitTemplate(template: t.Node[], position: number): Array<t.Node|e.AST> {
|
||||||
const visitor = new TemplateTargetVisitor(position);
|
const visitor = new TemplateTargetVisitor(position);
|
||||||
visitor.visitAll(template);
|
visitor.visitAll(template);
|
||||||
return visitor.path;
|
const {path} = visitor;
|
||||||
|
|
||||||
|
const strictPath = path.filter(v => v !== OUTSIDE_K_V_MARKER);
|
||||||
|
const candidate = strictPath[strictPath.length - 1];
|
||||||
|
const matchedASourceSpanButNotAKvSpan = path.some(v => v === OUTSIDE_K_V_MARKER);
|
||||||
|
if (matchedASourceSpanButNotAKvSpan &&
|
||||||
|
(candidate instanceof t.Template || candidate instanceof t.Element)) {
|
||||||
|
// Template nodes with key and value spans are always defined on a `t.Template` or
|
||||||
|
// `t.Element`. If we found a node on a template with a `sourceSpan` that includes the cursor,
|
||||||
|
// it is possible that we are outside the k/v spans (i.e. in-between them). If this is the
|
||||||
|
// case and we do not have any other candidate matches on the `t.Element` or `t.Template`, we
|
||||||
|
// want to return no results. Otherwise, the `t.Element`/`t.Template` result is incorrect for
|
||||||
|
// that cursor position.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return strictPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position must be absolute in the source file.
|
// Position must be absolute in the source file.
|
||||||
|
@ -229,7 +236,15 @@ class TemplateTargetVisitor implements t.Visitor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {start, end} = getSpanIncludingEndTag(node);
|
const {start, end} = getSpanIncludingEndTag(node);
|
||||||
if (isWithin(this.position, {start, end})) {
|
if (!isWithin(this.position, {start, end})) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTemplateNodeWithKeyAndValue(node) && !isWithinKeyValue(this.position, node)) {
|
||||||
|
// If cursor is within source span but not within key span or value span,
|
||||||
|
// do not return the node.
|
||||||
|
this.path.push(OUTSIDE_K_V_MARKER);
|
||||||
|
} else {
|
||||||
this.path.push(node);
|
this.path.push(node);
|
||||||
node.visit(this);
|
node.visit(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -598,6 +598,15 @@ describe('findNodeAtPosition for microsyntax expression', () => {
|
||||||
expect(node).toBeInstanceOf(e.PropertyRead);
|
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should locate property read next to variable in structural directive syntax', () => {
|
||||||
|
const {errors, nodes, position} = parse(`<div *ngIf="fo¦o as bar"></div>`);
|
||||||
|
expect(errors).toBe(null);
|
||||||
|
const {nodeInContext} = getTargetAtPosition(nodes, position)!;
|
||||||
|
const {node} = nodeInContext;
|
||||||
|
expect(isExpressionNode(node!)).toBe(true);
|
||||||
|
expect(node).toBeInstanceOf(e.PropertyRead);
|
||||||
|
});
|
||||||
|
|
||||||
it('should locate text attribute', () => {
|
it('should locate text attribute', () => {
|
||||||
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
|
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
|
||||||
// ngFor is a text attribute because the desugared form is
|
// ngFor is a text attribute because the desugared form is
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AbsoluteSourceSpan, CssSelector, ParseSourceSpan, SelectorMatcher} from '@angular/compiler';
|
import {AbsoluteSourceSpan, CssSelector, ParseSourceSpan, SelectorMatcher, TmplAstBoundEvent} from '@angular/compiler';
|
||||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
|
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
|
||||||
import {DeclarationNode} from '@angular/compiler-cli/src/ngtsc/reflection';
|
import {DeclarationNode} from '@angular/compiler-cli/src/ngtsc/reflection';
|
||||||
|
@ -54,6 +54,16 @@ export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeW
|
||||||
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
|
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isWithinKeyValue(position: number, node: NodeWithKeyAndValue): boolean {
|
||||||
|
let {keySpan, valueSpan} = node;
|
||||||
|
if (valueSpan === undefined && node instanceof TmplAstBoundEvent) {
|
||||||
|
valueSpan = node.handlerSpan;
|
||||||
|
}
|
||||||
|
const isWithinKeyValue =
|
||||||
|
isWithin(position, keySpan) || !!(valueSpan && isWithin(position, valueSpan));
|
||||||
|
return isWithinKeyValue;
|
||||||
|
}
|
||||||
|
|
||||||
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
||||||
// Template node implements the Node interface so we cannot use instanceof.
|
// Template node implements the Node interface so we cannot use instanceof.
|
||||||
return node.sourceSpan instanceof ParseSourceSpan;
|
return node.sourceSpan instanceof ParseSourceSpan;
|
||||||
|
|
Loading…
Reference in New Issue