feat(language-service): directive info when looking up attribute's symbol (#33127)
Now, hovering over an attribute on an element will provide information about the directive that attribute matches in the element, if any. (More generally, we return information about directive symbols matched on an element attribute.) I believe this is similar to how the indexer provides this kind of information, though more precise in the sense that this commit provides directive information only if the directive selector exactly matches the attribute selector. In another sense, this is a limitation. In fact, there are the limitations of: - Directives matched on the element, but with a selector of anything more than the attribute (e.g. `div[string-model]` or `[string-model][other-attr]`) will not be returned as symbols matching on the attribute. - Only one symbol can be returned currently. If the attribute matches multiple directives, only one directive symbol will be returned. Furthermore, we cannot say that the directive symbol returned is determinstic. Resolution of these limitations can be discussed in the future. At least the second limitation should be very easy to fixup in a future commit. This relies solely on the template compiler and is agnostic to any Ivy changes, so this is strictly a feature enhancement that will not have to be refactored when we migrate the language service to Ivy. PR Close #33127
This commit is contained in:
parent
6bc016f3fa
commit
ce7f934c66
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSummary, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler';
|
||||
import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSummary, CssSelector, DirectiveAst, ElementAst, SelectorMatcher, TemplateAstPath, findNode, tokenReference} from '@angular/compiler';
|
||||
import {getExpressionScope} from '@angular/compiler-cli/src/language_services';
|
||||
|
||||
import {AstResult} from './common';
|
||||
|
@ -88,7 +88,28 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde
|
|||
}
|
||||
},
|
||||
visitElementProperty(ast) { attributeValueSymbol(ast.value); },
|
||||
visitAttr(ast) {},
|
||||
visitAttr(ast) {
|
||||
const element = path.head;
|
||||
if (!element || !(element instanceof ElementAst)) return;
|
||||
// Create a mapping of all directives applied to the element from their selectors.
|
||||
const matcher = new SelectorMatcher<DirectiveAst>();
|
||||
for (const dir of element.directives) {
|
||||
if (!dir.directive.selector) continue;
|
||||
matcher.addSelectables(CssSelector.parse(dir.directive.selector), dir);
|
||||
}
|
||||
|
||||
// See if this attribute matches the selector of any directive on the element.
|
||||
// TODO(ayazhafiz): Consider caching selector matches (at the expense of potentially
|
||||
// very high memory usage).
|
||||
const attributeSelector = `[${ast.name}=${ast.value}]`;
|
||||
const parsedAttribute = CssSelector.parse(attributeSelector);
|
||||
if (!parsedAttribute.length) return;
|
||||
matcher.match(parsedAttribute[0], (_, directive) => {
|
||||
symbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
|
||||
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE);
|
||||
span = spanOf(ast);
|
||||
});
|
||||
},
|
||||
visitBoundText(ast) {
|
||||
const expressionPosition = templatePosition - ast.sourceSpan.start.offset;
|
||||
if (inSpan(expressionPosition, ast.value.span)) {
|
||||
|
|
|
@ -106,6 +106,20 @@ describe('hover', () => {
|
|||
expect(toText(displayParts)).toBe('(component) AppModule.TestComponent: class');
|
||||
});
|
||||
|
||||
it('should be able to find a reference to a directive', () => {
|
||||
const fileName = mockHost.addCode(`
|
||||
@Component({
|
||||
template: '<test-comp «string-model»></test-comp>'
|
||||
})
|
||||
export class MyComponent { }`);
|
||||
const marker = mockHost.getReferenceMarkerFor(fileName, 'string-model');
|
||||
const quickInfo = ngLS.getHoverAt(fileName, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo !;
|
||||
expect(textSpan).toEqual(marker);
|
||||
expect(toText(displayParts)).toBe('(directive) StringModel');
|
||||
});
|
||||
|
||||
it('should be able to find an event provider', () => {
|
||||
const fileName = mockHost.addCode(`
|
||||
@Component({
|
||||
|
|
Loading…
Reference in New Issue