feat(language-service): provide hover for microsyntax in structural directive (#34847)
PR Close #34847
This commit is contained in:
parent
0a1af2ff36
commit
e7dff9eb0c
|
@ -55,19 +55,14 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult):
|
||||||
let symbol: Symbol|undefined;
|
let symbol: Symbol|undefined;
|
||||||
let span: Span|undefined;
|
let span: Span|undefined;
|
||||||
let staticSymbol: StaticSymbol|undefined;
|
let staticSymbol: StaticSymbol|undefined;
|
||||||
const attributeValueSymbol = (ast: AST): boolean => {
|
const attributeValueSymbol = (): boolean => {
|
||||||
const attribute = findAttribute(info, position);
|
const attribute = findAttribute(info, position);
|
||||||
if (attribute) {
|
if (attribute) {
|
||||||
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
|
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
|
||||||
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
const result = getSymbolInAttributeValue(info, path, attribute);
|
||||||
const scope = getExpressionScope(dinfo, path);
|
|
||||||
if (attribute.valueSpan) {
|
|
||||||
const result = getExpressionSymbol(scope, ast, templatePosition, info.template.query);
|
|
||||||
if (result) {
|
if (result) {
|
||||||
symbol = result.symbol;
|
symbol = result.symbol;
|
||||||
const expressionOffset = attribute.valueSpan.start.offset;
|
span = offsetSpan(result.span, attribute.valueSpan !.start.offset);
|
||||||
span = offsetSpan(result.span, expressionOffset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -105,13 +100,13 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult):
|
||||||
},
|
},
|
||||||
visitVariable(ast) {},
|
visitVariable(ast) {},
|
||||||
visitEvent(ast) {
|
visitEvent(ast) {
|
||||||
if (!attributeValueSymbol(ast.handler)) {
|
if (!attributeValueSymbol()) {
|
||||||
symbol = findOutputBinding(info, path, ast);
|
symbol = findOutputBinding(info, path, ast);
|
||||||
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.EVENT);
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.EVENT);
|
||||||
span = spanOf(ast);
|
span = spanOf(ast);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visitElementProperty(ast) { attributeValueSymbol(ast.value); },
|
visitElementProperty(ast) { attributeValueSymbol(); },
|
||||||
visitAttr(ast) {
|
visitAttr(ast) {
|
||||||
const element = path.head;
|
const element = path.head;
|
||||||
if (!element || !(element instanceof ElementAst)) return;
|
if (!element || !(element instanceof ElementAst)) return;
|
||||||
|
@ -155,11 +150,24 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult):
|
||||||
span = spanOf(ast);
|
span = spanOf(ast);
|
||||||
},
|
},
|
||||||
visitDirectiveProperty(ast) {
|
visitDirectiveProperty(ast) {
|
||||||
if (!attributeValueSymbol(ast.value)) {
|
if (!attributeValueSymbol()) {
|
||||||
symbol = findInputBinding(info, templatePosition, ast);
|
const directive = findParentOfBinding(info.templateAst, ast, templatePosition);
|
||||||
|
const attribute = findAttribute(info, position);
|
||||||
|
if (directive && attribute) {
|
||||||
|
if (attribute.name.startsWith('*')) {
|
||||||
|
const compileTypeSummary = directive.directive;
|
||||||
|
symbol = info.template.query.getTypeSymbol(compileTypeSummary.type.reference);
|
||||||
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE);
|
||||||
|
// Use 'attribute.sourceSpan' instead of the directive's,
|
||||||
|
// because the span of the directive is the whole opening tag of an element.
|
||||||
|
span = spanOf(attribute.sourceSpan);
|
||||||
|
} else {
|
||||||
|
symbol = findInputBinding(info, ast.templateName, directive);
|
||||||
span = spanOf(ast);
|
span = spanOf(ast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
null);
|
null);
|
||||||
if (symbol && span) {
|
if (symbol && span) {
|
||||||
|
@ -171,6 +179,42 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the symbol in attribute value at template position.
|
||||||
|
function getSymbolInAttributeValue(info: AstResult, path: TemplateAstPath, attribute: Attribute):
|
||||||
|
{symbol: Symbol, span: Span}|undefined {
|
||||||
|
if (!attribute.valueSpan) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Find the symbol that contains the position.
|
||||||
|
templateBindings.filter(tb => !tb.keyIsVar).forEach(tb => {
|
||||||
|
if (inSpan(valueRelativePosition, tb.expression?.ast.span)) {
|
||||||
|
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
||||||
|
const scope = getExpressionScope(dinfo, path);
|
||||||
|
result = getExpressionSymbol(scope, tb.expression !, path.position, info.template.query);
|
||||||
|
} else if (inSpan(valueRelativePosition, tb.span)) {
|
||||||
|
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);
|
||||||
|
if (symbol) {
|
||||||
|
result = {symbol, span: tb.span};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function findAttribute(info: AstResult, position: number): Attribute|undefined {
|
function findAttribute(info: AstResult, position: number): Attribute|undefined {
|
||||||
const templatePosition = position - info.template.span.start;
|
const templatePosition = position - info.template.span.start;
|
||||||
const path = getPathToNodeAtPosition(info.htmlAst, templatePosition);
|
const path = getPathToNodeAtPosition(info.htmlAst, templatePosition);
|
||||||
|
@ -222,12 +266,11 @@ function findParentOfBinding(
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findInputBinding(
|
// Find the symbol of input binding in 'directiveAst' by 'name'.
|
||||||
info: AstResult, position: number, binding: BoundDirectivePropertyAst): Symbol|undefined {
|
function findInputBinding(info: AstResult, name: string, directiveAst: DirectiveAst): Symbol|
|
||||||
const directiveAst = findParentOfBinding(info.templateAst, binding, position);
|
undefined {
|
||||||
if (directiveAst) {
|
|
||||||
const invertedInput = invertMap(directiveAst.directive.inputs);
|
const invertedInput = invertMap(directiveAst.directive.inputs);
|
||||||
const fieldName = invertedInput[binding.templateName];
|
const fieldName = invertedInput[name];
|
||||||
if (fieldName) {
|
if (fieldName) {
|
||||||
const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference);
|
const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference);
|
||||||
if (classSymbol) {
|
if (classSymbol) {
|
||||||
|
@ -235,7 +278,6 @@ function findInputBinding(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function findOutputBinding(info: AstResult, path: TemplateAstPath, binding: BoundEventAst): Symbol|
|
function findOutputBinding(info: AstResult, path: TemplateAstPath, binding: BoundEventAst): Symbol|
|
||||||
undefined {
|
undefined {
|
||||||
|
|
|
@ -266,11 +266,13 @@ describe('definitions', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to find a structural directive', () => {
|
describe('in structural directive', () => {
|
||||||
mockHost.override(TEST_TEMPLATE, `<div ~{start-my}*«ngIf»="true"~{end-my}></div>`);
|
it('should be able to find the directive', () => {
|
||||||
|
mockHost.override(
|
||||||
|
TEST_TEMPLATE, `<div ~{start-my}*«ngFor»="let item of heroes;"~{end-my}></div>`);
|
||||||
|
|
||||||
// Get the marker for ngIf in the code added above.
|
// Get the marker for ngFor in the code added above.
|
||||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'ngIf');
|
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'ngFor');
|
||||||
|
|
||||||
const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start);
|
const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
|
@ -286,11 +288,66 @@ describe('definitions', () => {
|
||||||
const refFileName = '/node_modules/@angular/common/common.d.ts';
|
const refFileName = '/node_modules/@angular/common/common.d.ts';
|
||||||
const def = definitions ![0];
|
const def = definitions ![0];
|
||||||
expect(def.fileName).toBe(refFileName);
|
expect(def.fileName).toBe(refFileName);
|
||||||
expect(def.name).toBe('ngIf');
|
expect(def.name).toBe('NgForOf');
|
||||||
expect(def.kind).toBe('property');
|
expect(def.kind).toBe('directive');
|
||||||
// Not asserting the textSpan of definition because it's external file
|
// Not asserting the textSpan of definition because it's external file
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>`);
|
||||||
|
|
||||||
|
// Get the marker for trackBy in the code added above.
|
||||||
|
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'trackBy');
|
||||||
|
|
||||||
|
const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
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(definitions).toBeDefined();
|
||||||
|
// The two definitions are setter and getter of 'ngForTrackBy'.
|
||||||
|
expect(definitions !.length).toBe(4);
|
||||||
|
|
||||||
|
const refFileName = '/node_modules/@angular/common/common.d.ts';
|
||||||
|
definitions !.forEach(def => {
|
||||||
|
expect(def.fileName).toBe(refFileName);
|
||||||
|
expect(def.name).toBe('ngForTrackBy');
|
||||||
|
expect(def.kind).toBe('method');
|
||||||
|
});
|
||||||
|
// Not asserting the textSpan of definition because it's external file
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to find the property value', () => {
|
||||||
|
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let item of «heroes»; trackBy: test;"></div>`);
|
||||||
|
|
||||||
|
// Get the marker for heroes in the code added above.
|
||||||
|
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'heroes');
|
||||||
|
|
||||||
|
const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
const {textSpan, definitions} = result !;
|
||||||
|
|
||||||
|
expect(textSpan).toEqual(marker);
|
||||||
|
|
||||||
|
expect(definitions).toBeDefined();
|
||||||
|
expect(definitions !.length).toBe(2);
|
||||||
|
|
||||||
|
const refFileName = '/app/parsing-cases.ts';
|
||||||
|
const def = definitions ![0];
|
||||||
|
expect(def.fileName).toBe(refFileName);
|
||||||
|
expect(def.name).toBe('heroes');
|
||||||
|
expect(def.kind).toBe('property');
|
||||||
|
const content = mockHost.readFile(refFileName) !;
|
||||||
|
expect(content.substring(def.textSpan.start, def.textSpan.start + def.textSpan.length))
|
||||||
|
.toEqual(`heroes: Hero[] = [this.hero];`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to find a two-way binding', () => {
|
it('should be able to find a two-way binding', () => {
|
||||||
mockHost.override(
|
mockHost.override(
|
||||||
TEST_TEMPLATE,
|
TEST_TEMPLATE,
|
||||||
|
|
|
@ -141,14 +141,37 @@ describe('hover', () => {
|
||||||
expect(toText(displayParts)).toBe('(property) TestComponent.name: string');
|
expect(toText(displayParts)).toBe('(property) TestComponent.name: string');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to find a structural directive', () => {
|
describe('over structural directive', () => {
|
||||||
mockHost.override(TEST_TEMPLATE, `<div «*ᐱngIfᐱ="true"»></div>`);
|
it('should be able to find the directive', () => {
|
||||||
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'ngIf');
|
mockHost.override(TEST_TEMPLATE, `<div «*ᐱngForᐱ="let item of heroes"»></div>`);
|
||||||
|
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'ngFor');
|
||||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||||
expect(quickInfo).toBeTruthy();
|
expect(quickInfo).toBeTruthy();
|
||||||
const {textSpan, displayParts} = quickInfo !;
|
const {textSpan, displayParts} = quickInfo !;
|
||||||
expect(textSpan).toEqual(marker);
|
expect(textSpan).toEqual(marker);
|
||||||
expect(toText(displayParts)).toBe('(property) NgIf<T>.ngIf: T');
|
expect(toText(displayParts)).toBe('(directive) NgForOf: typeof NgForOf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to find the directive property', () => {
|
||||||
|
mockHost.override(
|
||||||
|
TEST_TEMPLATE, `<div *ngFor="let item of heroes; «ᐱtrackByᐱ: test»;"></div>`);
|
||||||
|
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'trackBy');
|
||||||
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(quickInfo).toBeTruthy();
|
||||||
|
const {textSpan, displayParts} = quickInfo !;
|
||||||
|
expect(textSpan).toEqual(marker);
|
||||||
|
expect(toText(displayParts)).toBe('(method) NgForOf<T, U>.ngForTrackBy: TrackByFunction<T>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to find the property value', () => {
|
||||||
|
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let item of «heroes»; trackBy: test;"></div>`);
|
||||||
|
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'heroes');
|
||||||
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(quickInfo).toBeTruthy();
|
||||||
|
const {textSpan, displayParts} = quickInfo !;
|
||||||
|
expect(textSpan).toEqual(marker);
|
||||||
|
expect(toText(displayParts)).toBe('(property) TemplateReference.heroes: Hero[]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to find a reference to a two-way binding', () => {
|
it('should be able to find a reference to a two-way binding', () => {
|
||||||
|
|
Loading…
Reference in New Issue