From e7dff9eb0c3b8304424772e05d4274f72a20c9b3 Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Fri, 17 Jan 2020 18:11:23 +0800 Subject: [PATCH] feat(language-service): provide hover for microsyntax in structural directive (#34847) PR Close #34847 --- .../language-service/src/locate_symbol.ts | 94 ++++++++++++++----- .../language-service/test/definitions_spec.ts | 93 ++++++++++++++---- packages/language-service/test/hover_spec.ts | 39 ++++++-- 3 files changed, 174 insertions(+), 52 deletions(-) diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts index 68773e6b9f..d46e542896 100644 --- a/packages/language-service/src/locate_symbol.ts +++ b/packages/language-service/src/locate_symbol.ts @@ -55,19 +55,14 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult): let symbol: Symbol|undefined; let span: Span|undefined; let staticSymbol: StaticSymbol|undefined; - const attributeValueSymbol = (ast: AST): boolean => { + const attributeValueSymbol = (): boolean => { const attribute = findAttribute(info, position); if (attribute) { if (inSpan(templatePosition, spanOf(attribute.valueSpan))) { - const dinfo = diagnosticInfoFromTemplateInfo(info); - const scope = getExpressionScope(dinfo, path); - if (attribute.valueSpan) { - const result = getExpressionSymbol(scope, ast, templatePosition, info.template.query); - if (result) { - symbol = result.symbol; - const expressionOffset = attribute.valueSpan.start.offset; - span = offsetSpan(result.span, expressionOffset); - } + const result = getSymbolInAttributeValue(info, path, attribute); + if (result) { + symbol = result.symbol; + span = offsetSpan(result.span, attribute.valueSpan !.start.offset); } return true; } @@ -105,13 +100,13 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult): }, visitVariable(ast) {}, visitEvent(ast) { - if (!attributeValueSymbol(ast.handler)) { + if (!attributeValueSymbol()) { symbol = findOutputBinding(info, path, ast); symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.EVENT); span = spanOf(ast); } }, - visitElementProperty(ast) { attributeValueSymbol(ast.value); }, + visitElementProperty(ast) { attributeValueSymbol(); }, visitAttr(ast) { const element = path.head; if (!element || !(element instanceof ElementAst)) return; @@ -155,9 +150,22 @@ function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult): span = spanOf(ast); }, visitDirectiveProperty(ast) { - if (!attributeValueSymbol(ast.value)) { - symbol = findInputBinding(info, templatePosition, ast); - span = spanOf(ast); + if (!attributeValueSymbol()) { + 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); + } + } } } }, @@ -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 { const templatePosition = position - info.template.span.start; const path = getPathToNodeAtPosition(info.htmlAst, templatePosition); @@ -222,17 +266,15 @@ function findParentOfBinding( return res; } -function findInputBinding( - info: AstResult, position: number, binding: BoundDirectivePropertyAst): Symbol|undefined { - const directiveAst = findParentOfBinding(info.templateAst, binding, position); - if (directiveAst) { - const invertedInput = invertMap(directiveAst.directive.inputs); - const fieldName = invertedInput[binding.templateName]; - if (fieldName) { - const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference); - if (classSymbol) { - return classSymbol.members().get(fieldName); - } +// Find the symbol of input binding in 'directiveAst' by 'name'. +function findInputBinding(info: AstResult, name: string, directiveAst: DirectiveAst): Symbol| + undefined { + const invertedInput = invertMap(directiveAst.directive.inputs); + const fieldName = invertedInput[name]; + if (fieldName) { + const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference); + if (classSymbol) { + return classSymbol.members().get(fieldName); } } } diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts index 25bc24a60a..44c8aaab16 100644 --- a/packages/language-service/test/definitions_spec.ts +++ b/packages/language-service/test/definitions_spec.ts @@ -266,29 +266,86 @@ describe('definitions', () => { } }); - it('should be able to find a structural directive', () => { - mockHost.override(TEST_TEMPLATE, `
`); + describe('in structural directive', () => { + it('should be able to find the directive', () => { + mockHost.override( + TEST_TEMPLATE, `
`); - // Get the marker for ngIf in the code added above. - const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'ngIf'); + // Get the marker for ngFor in the code added above. + const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'ngFor'); - const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start); - expect(result).toBeDefined(); - const {textSpan, definitions} = result !; + 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); + // 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(); - expect(definitions !.length).toBe(1); + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); - const refFileName = '/node_modules/@angular/common/common.d.ts'; - const def = definitions ![0]; - expect(def.fileName).toBe(refFileName); - expect(def.name).toBe('ngIf'); - expect(def.kind).toBe('property'); - // Not asserting the textSpan of definition because it's external file + const refFileName = '/node_modules/@angular/common/common.d.ts'; + const def = definitions ![0]; + expect(def.fileName).toBe(refFileName); + expect(def.name).toBe('NgForOf'); + expect(def.kind).toBe('directive'); + // Not asserting the textSpan of definition because it's external file + }); + + it('should be able to find the directive property', () => { + mockHost.override( + TEST_TEMPLATE, + `
`); + + // 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, `
`); + + // 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', () => { diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index 2342e0edd2..578f7fcc7e 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -141,14 +141,37 @@ describe('hover', () => { expect(toText(displayParts)).toBe('(property) TestComponent.name: string'); }); - it('should be able to find a structural directive', () => { - mockHost.override(TEST_TEMPLATE, `
`); - const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'ngIf'); - const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); - expect(quickInfo).toBeTruthy(); - const {textSpan, displayParts} = quickInfo !; - expect(textSpan).toEqual(marker); - expect(toText(displayParts)).toBe('(property) NgIf.ngIf: T'); + describe('over structural directive', () => { + it('should be able to find the directive', () => { + mockHost.override(TEST_TEMPLATE, `
`); + const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'ngFor'); + const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(directive) NgForOf: typeof NgForOf'); + }); + + it('should be able to find the directive property', () => { + mockHost.override( + TEST_TEMPLATE, `
`); + 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.ngForTrackBy: TrackByFunction'); + }); + + it('should be able to find the property value', () => { + mockHost.override(TEST_TEMPLATE, `
`); + 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', () => {