diff --git a/integration/language_service_plugin/goldens/definitionAndBoundSpan.json b/integration/language_service_plugin/goldens/definitionAndBoundSpan.json index b5c8560563..c7cc7cc9f0 100644 --- a/integration/language_service_plugin/goldens/definitionAndBoundSpan.json +++ b/integration/language_service_plugin/goldens/definitionAndBoundSpan.json @@ -20,12 +20,12 @@ ], "textSpan": { "start": { - "line": 7, - "offset": 30 + "line": 5, + "offset": 26 }, "end": { - "line": 7, - "offset": 47 + "line": 5, + "offset": 30 } } } diff --git a/integration/language_service_plugin/goldens/quickinfo.json b/integration/language_service_plugin/goldens/quickinfo.json index 9bd1f4a75f..5171068d64 100644 --- a/integration/language_service_plugin/goldens/quickinfo.json +++ b/integration/language_service_plugin/goldens/quickinfo.json @@ -5,7 +5,7 @@ "request_seq": 2, "success": true, "body": { - "kind": "", + "kind": "property", "kindModifiers": "", "start": { "line": 5, @@ -15,7 +15,7 @@ "line": 5, "offset": 30 }, - "displayString": "property name of AppComponent", + "displayString": "(property) AppComponent.name", "documentation": "", "tags": [] } diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index ae3e0cc241..297861d6f8 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -6,28 +6,50 @@ * found in the LICENSE file at https://angular.io/license */ -import * as tss from 'typescript/lib/tsserverlibrary'; - +import * as ts from 'typescript'; // used as value and is provided at runtime import {TemplateInfo} from './common'; import {locateSymbol} from './locate_symbol'; -import {Location} from './types'; +import {Span} from './types'; -export function getDefinition(info: TemplateInfo): Location[]|undefined { - const result = locateSymbol(info); - return result && result.symbol.definition; -} - -export function ngLocationToTsDefinitionInfo(loc: Location): tss.DefinitionInfo { +/** + * Convert Angular Span to TypeScript TextSpan. Angular Span has 'start' and + * 'end' whereas TS TextSpan has 'start' and 'length'. + * @param span Angular Span + */ +function ngSpanToTsTextSpan(span: Span): ts.TextSpan { return { - fileName: loc.fileName, - textSpan: { - start: loc.span.start, - length: loc.span.end - loc.span.start, - }, - // TODO(kyliau): Provide more useful info for name, kind and containerKind - name: '', // should be name of symbol but we don't have enough information here. - kind: tss.ScriptElementKind.unknown, - containerName: loc.fileName, - containerKind: tss.ScriptElementKind.unknown, + start: span.start, + length: span.end - span.start, + }; +} + +export function getDefinitionAndBoundSpan(info: TemplateInfo): ts.DefinitionInfoAndBoundSpan| + undefined { + const symbolInfo = locateSymbol(info); + if (!symbolInfo) { + return; + } + const textSpan = ngSpanToTsTextSpan(symbolInfo.span); + const {symbol} = symbolInfo; + const {container, definition: locations} = symbol; + if (!locations || !locations.length) { + // symbol.definition is really the locations of the symbol. There could be + // more than one. No meaningful info could be provided without any location. + return {textSpan}; + } + const containerKind = container ? container.kind : ts.ScriptElementKind.unknown; + const containerName = container ? container.name : ''; + const definitions = locations.map((location) => { + return { + kind: symbol.kind as ts.ScriptElementKind, + name: symbol.name, + containerKind: containerKind as ts.ScriptElementKind, + containerName: containerName, + textSpan: ngSpanToTsTextSpan(location.span), + fileName: location.fileName, + }; + }); + return { + definitions, textSpan, }; } diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts index bc28e2f349..d1f6f50990 100644 --- a/packages/language-service/src/hover.ts +++ b/packages/language-service/src/hover.ts @@ -6,23 +6,42 @@ * found in the LICENSE file at https://angular.io/license */ +import * as ts from 'typescript'; import {TemplateInfo} from './common'; import {locateSymbol} from './locate_symbol'; -import {Hover, HoverTextSection, Symbol} from './types'; -export function getHover(info: TemplateInfo): Hover|undefined { - const result = locateSymbol(info); - if (result) { - return {text: hoverTextOf(result.symbol), span: result.span}; +// Reverse mappings of enum would generate strings +const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; +const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; + +export function getHover(info: TemplateInfo): ts.QuickInfo|undefined { + const symbolInfo = locateSymbol(info); + if (!symbolInfo) { + return; } + const {symbol, span} = symbolInfo; + const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ? + [ + {text: symbol.container.name, kind: symbol.container.kind}, + {text: '.', kind: SYMBOL_PUNC}, + ] : + []; + return { + kind: symbol.kind as ts.ScriptElementKind, + kindModifiers: '', // kindModifier info not available on 'ng.Symbol' + textSpan: { + start: span.start, + length: span.end - span.start, + }, + // this would generate a string like '(property) ClassX.propY' + // 'kind' in displayParts does not really matter because it's dropped when + // displayParts get converted to string. + displayParts: [ + {text: '(', kind: SYMBOL_PUNC}, {text: symbol.kind, kind: symbol.kind}, + {text: ')', kind: SYMBOL_PUNC}, {text: ' ', kind: SYMBOL_SPACE}, ...containerDisplayParts, + {text: symbol.name, kind: symbol.kind}, + // TODO: Append type info as well, but Symbol doesn't expose that! + // Ideally hover text should be like '(property) ClassX.propY: string' + ], + }; } - -function hoverTextOf(symbol: Symbol): HoverTextSection[] { - const result: HoverTextSection[] = - [{text: symbol.kind}, {text: ' '}, {text: symbol.name, language: symbol.language}]; - const container = symbol.container; - if (container) { - result.push({text: ' of '}, {text: container.name, language: container.language}); - } - return result; -} \ No newline at end of file diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index b0a341cb5f..13866d7df4 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -8,9 +8,9 @@ import {CompileMetadataResolver, CompilePipeSummary} from '@angular/compiler'; import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; - +import * as tss from 'typescript/lib/tsserverlibrary'; import {getTemplateCompletions} from './completions'; -import {getDefinition} from './definitions'; +import {getDefinitionAndBoundSpan} from './definitions'; import {getDeclarationDiagnostics} from './diagnostics'; import {getHover} from './hover'; import {Completion, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Location, Span, TemplateSource} from './types'; @@ -30,8 +30,6 @@ export function createLanguageService(host: LanguageServiceHost): LanguageServic class LanguageServiceImpl implements LanguageService { constructor(private host: LanguageServiceHost) {} - private get metadataResolver(): CompileMetadataResolver { return this.host.resolver; } - getTemplateReferences(): string[] { return this.host.getTemplateReferences(); } getDiagnostics(fileName: string): Diagnostic[] { @@ -65,14 +63,14 @@ class LanguageServiceImpl implements LanguageService { } } - getDefinitionAt(fileName: string, position: number): Location[]|undefined { + getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined { let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { - return getDefinition(templateInfo); + return getDefinitionAndBoundSpan(templateInfo); } } - getHoverAt(fileName: string, position: number): Hover|undefined { + getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined { let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { return getHover(templateInfo); diff --git a/packages/language-service/src/ts_plugin.ts b/packages/language-service/src/ts_plugin.ts index eb41009748..3622dd9599 100644 --- a/packages/language-service/src/ts_plugin.ts +++ b/packages/language-service/src/ts_plugin.ts @@ -9,9 +9,8 @@ import * as ts from 'typescript'; // used as value, passed in by tsserver at runtime import * as tss from 'typescript/lib/tsserverlibrary'; // used as type only -import {ngLocationToTsDefinitionInfo} from './definitions'; import {createLanguageService} from './language_service'; -import {Completion, Diagnostic, DiagnosticMessageChain, Location} from './types'; +import {Completion, Diagnostic, DiagnosticMessageChain} from './types'; import {TypeScriptServiceHost} from './typescript_host'; const projectHostMap = new WeakMap(); @@ -76,13 +75,13 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService { // This effectively disables native TS features and is meant for internal // use only. const angularOnly = config ? config.angularOnly === true : false; - const proxy: tss.LanguageService = Object.assign({}, tsLS); const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS); const ngLS = createLanguageService(ngLSHost); projectHostMap.set(project, ngLSHost); - proxy.getCompletionsAtPosition = function( - fileName: string, position: number, options: tss.GetCompletionsAtPositionOptions|undefined) { + function getCompletionsAtPosition( + fileName: string, position: number, + options: tss.GetCompletionsAtPositionOptions | undefined) { if (!angularOnly) { const results = tsLS.getCompletionsAtPosition(fileName, position, options); if (results && results.entries.length) { @@ -100,39 +99,20 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService { isNewIdentifierLocation: false, entries: results.map(completionToEntry), }; - }; + } - proxy.getQuickInfoAtPosition = function(fileName: string, position: number): tss.QuickInfo | - undefined { - if (!angularOnly) { - const result = tsLS.getQuickInfoAtPosition(fileName, position); - if (result) { - // If TS could answer the query, then return results immediately. - return result; - } - } - const result = ngLS.getHoverAt(fileName, position); - if (!result) { - return; - } - return { - // TODO(kyliau): Provide more useful info for kind and kindModifiers - kind: ts.ScriptElementKind.unknown, - kindModifiers: ts.ScriptElementKindModifier.none, - textSpan: { - start: result.span.start, - length: result.span.end - result.span.start, - }, - displayParts: result.text.map((part) => { - return { - text: part.text, - kind: part.language || 'angular', - }; - }), - }; - }; + function getQuickInfoAtPosition(fileName: string, position: number): tss.QuickInfo|undefined { + if (!angularOnly) { + const result = tsLS.getQuickInfoAtPosition(fileName, position); + if (result) { + // If TS could answer the query, then return results immediately. + return result; + } + } + return ngLS.getHoverAt(fileName, position); + } - proxy.getSemanticDiagnostics = function(fileName: string): tss.Diagnostic[] { + function getSemanticDiagnostics(fileName: string): tss.Diagnostic[] { const results: tss.Diagnostic[] = []; if (!angularOnly) { const tsResults = tsLS.getSemanticDiagnostics(fileName); @@ -146,48 +126,43 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService { const sourceFile = fileName.endsWith('.ts') ? ngLSHost.getSourceFile(fileName) : undefined; results.push(...ngResults.map(d => diagnosticToDiagnostic(d, sourceFile))); return results; - }; + } - proxy.getDefinitionAtPosition = function(fileName: string, position: number): - ReadonlyArray| - undefined { - if (!angularOnly) { - const results = tsLS.getDefinitionAtPosition(fileName, position); - if (results) { - // If TS could answer the query, then return results immediately. - return results; - } - } - const results = ngLS.getDefinitionAt(fileName, position); - if (!results) { - return; - } - return results.map(ngLocationToTsDefinitionInfo); - }; + function getDefinitionAtPosition( + fileName: string, position: number): ReadonlyArray|undefined { + if (!angularOnly) { + const results = tsLS.getDefinitionAtPosition(fileName, position); + if (results) { + // If TS could answer the query, then return results immediately. + return results; + } + } + const result = ngLS.getDefinitionAt(fileName, position); + if (!result || !result.definitions || !result.definitions.length) { + return; + } + return result.definitions; + } - proxy.getDefinitionAndBoundSpan = function(fileName: string, position: number): - tss.DefinitionInfoAndBoundSpan | - undefined { - if (!angularOnly) { - const result = tsLS.getDefinitionAndBoundSpan(fileName, position); - if (result) { - // If TS could answer the query, then return results immediately. - return result; - } - } - const results = ngLS.getDefinitionAt(fileName, position); - if (!results || !results.length) { - return; - } - const {span} = results[0]; - return { - definitions: results.map(ngLocationToTsDefinitionInfo), - textSpan: { - start: span.start, - length: span.end - span.start, - }, - }; - }; + function getDefinitionAndBoundSpan( + fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined { + if (!angularOnly) { + const result = tsLS.getDefinitionAndBoundSpan(fileName, position); + if (result) { + // If TS could answer the query, then return results immediately. + return result; + } + } + return ngLS.getDefinitionAt(fileName, position); + } + const proxy: tss.LanguageService = Object.assign( + // First clone the original TS language service + {}, tsLS, + // Then override the methods supported by Angular language service + { + getCompletionsAtPosition, getQuickInfoAtPosition, getSemanticDiagnostics, + getDefinitionAtPosition, getDefinitionAndBoundSpan, + }); return proxy; } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index b6c25e245b..0132f65f10 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -8,6 +8,8 @@ import {CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli/src/language_services'; +import * as tss from 'typescript/lib/tsserverlibrary'; + import {AstResult, TemplateInfo} from './common'; export { @@ -394,12 +396,12 @@ export interface LanguageService { /** * Return the definition location for the symbol at position. */ - getDefinitionAt(fileName: string, position: number): Location[]|undefined; + getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined; /** * Return the hover information for the symbol at position. */ - getHoverAt(fileName: string, position: number): Hover|undefined; + getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined; /** * Return the pipes that are available at the given position. diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts index 752d0a0ba6..48f608e01b 100644 --- a/packages/language-service/test/definitions_spec.ts +++ b/packages/language-service/test/definitions_spec.ts @@ -9,159 +9,319 @@ import * as ts from 'typescript'; import {createLanguageService} from '../src/language_service'; -import {Span} from '../src/types'; +import {LanguageService} from '../src/types'; import {TypeScriptServiceHost} from '../src/typescript_host'; import {toh} from './test_data'; - -import {MockTypescriptHost,} from './test_utils'; +import {MockTypescriptHost} from './test_utils'; describe('definitions', () => { - let documentRegistry = ts.createDocumentRegistry(); - let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh); - let service = ts.createLanguageService(mockHost, documentRegistry); - let ngHost = new TypeScriptServiceHost(mockHost, service); - let ngService = createLanguageService(ngHost); + let mockHost: MockTypescriptHost; + let service: ts.LanguageService; + let ngHost: TypeScriptServiceHost; + let ngService: LanguageService; + + beforeEach(() => { + // Create a new mockHost every time to reset any files that are overridden. + mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh); + service = ts.createLanguageService(mockHost); + ngHost = new TypeScriptServiceHost(mockHost, service); + ngService = createLanguageService(ngHost); + }); it('should be able to find field in an interpolation', () => { - localReference( - ` @Component({template: '{{«name»}}'}) export class MyComponent { «ᐱnameᐱ: string;» }`); + const fileName = addCode(` + @Component({ + template: '{{«name»}}' + }) + export class MyComponent { + «ᐱnameᐱ: string;» + }`); + + const marker = getReferenceMarkerFor(fileName, 'name'); + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + expect(textSpan).toEqual(marker); + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + expect(def.fileName).toBe(fileName); + expect(def.name).toBe('name'); + expect(def.kind).toBe('property'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(fileName, 'name')); }); it('should be able to find a field in a attribute reference', () => { - localReference( - ` @Component({template: ''}) export class MyComponent { «ᐱnameᐱ: string;» }`); + const fileName = addCode(` + @Component({ + template: '' + }) + export class MyComponent { + «ᐱnameᐱ: string;» + }`); + + const marker = getReferenceMarkerFor(fileName, 'name'); + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + expect(textSpan).toEqual(marker); + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + expect(def.fileName).toBe(fileName); + expect(def.name).toBe('name'); + expect(def.kind).toBe('property'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(fileName, 'name')); }); it('should be able to find a method from a call', () => { - localReference( - ` @Component({template: '
'}) export class MyComponent { «ᐱmyClickᐱ() { }»}`); + const fileName = addCode(` + @Component({ + template: '
' + }) + export class MyComponent { + «ᐱmyClickᐱ() { }» + }`); + + const marker = getReferenceMarkerFor(fileName, 'myClick'); + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + expect(textSpan).toEqual(getLocationMarkerFor(fileName, 'my')); + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + expect(def.fileName).toBe(fileName); + expect(def.name).toBe('myClick'); + expect(def.kind).toBe('method'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(fileName, 'myClick')); }); it('should be able to find a field reference in an *ngIf', () => { - localReference( - ` @Component({template: '
'}) export class MyComponent { «ᐱincludeᐱ = true;»}`); + const fileName = addCode(` + @Component({ + template: '
' + }) + export class MyComponent { + «ᐱincludeᐱ = true;» + }`); + + const marker = getReferenceMarkerFor(fileName, 'include'); + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + expect(textSpan).toEqual(marker); + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + expect(def.fileName).toBe(fileName); + expect(def.name).toBe('include'); + expect(def.kind).toBe('property'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(fileName, 'include')); }); it('should be able to find a reference to a component', () => { - reference( - 'parsing-cases.ts', - ` @Component({template: '<«test-comp»>'}) export class MyComponent { }`); + const fileName = addCode(` + @Component({ + template: '~{start-my}<«test-comp»>~{end-my}' + }) + export class MyComponent { }`); + + // Get the marker for «test-comp» in the code added above. + const marker = getReferenceMarkerFor(fileName, 'test-comp'); + + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + // Get the marker for bounded text in the code added above. + const boundedText = getLocationMarkerFor(fileName, 'my'); + expect(textSpan).toEqual(boundedText); + + // There should be exactly 1 definition + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + const refFileName = '/app/parsing-cases.ts'; + expect(def.fileName).toBe(refFileName); + expect(def.name).toBe('TestComponent'); + expect(def.kind).toBe('component'); + expect(def.textSpan).toEqual(getLocationMarkerFor(refFileName, 'test-comp')); }); it('should be able to find an event provider', () => { - reference( - '/app/parsing-cases.ts', 'test', - ` @Component({template: ''}) export class MyComponent { myHandler() {} }`); + const fileName = addCode(` + @Component({ + template: '' + }) + export class MyComponent { myHandler() {} }`); + + // Get the marker for «test» in the code added above. + const marker = getReferenceMarkerFor(fileName, 'test'); + + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + // Get the marker for bounded text in the code added above + const boundedText = getLocationMarkerFor(fileName, 'my'); + expect(textSpan).toEqual(boundedText); + + // There should be exactly 1 definition + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + const refFileName = '/app/parsing-cases.ts'; + expect(def.fileName).toBe(refFileName); + expect(def.name).toBe('testEvent'); + expect(def.kind).toBe('event'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(refFileName, 'test')); }); it('should be able to find an input provider', () => { - reference( - '/app/parsing-cases.ts', 'tcName', - ` @Component({template: ''}) export class MyComponent { name = 'my name'; }`); + // '/app/parsing-cases.ts', 'tcName', + const fileName = addCode(` + @Component({ + template: '' + }) + export class MyComponent { + name = 'my name'; + }`); + + // Get the marker for «test» in the code added above. + const marker = getReferenceMarkerFor(fileName, 'tcName'); + + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + // Get the marker for bounded text in the code added above + const boundedText = getLocationMarkerFor(fileName, 'my'); + expect(textSpan).toEqual(boundedText); + + // There should be exactly 1 definition + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(1); + const def = definitions ![0]; + + const refFileName = '/app/parsing-cases.ts'; + expect(def.fileName).toBe(refFileName); + expect(def.name).toBe('name'); + expect(def.kind).toBe('property'); + expect(def.textSpan).toEqual(getDefinitionMarkerFor(refFileName, 'tcName')); }); it('should be able to find a pipe', () => { - reference( - 'common.d.ts', - ` @Component({template: '
'}) export class MyComponent { input: EventEmitter; }`); + const fileName = addCode(` + @Component({ + template: '
' + }) + export class MyComponent { + input: EventEmitter; + }`); + + // Get the marker for «test» in the code added above. + const marker = getReferenceMarkerFor(fileName, 'async'); + + const result = ngService.getDefinitionAt(fileName, marker.start); + expect(result).toBeDefined(); + const {textSpan, definitions} = result !; + + // Get the marker for bounded text in the code added above + const boundedText = getLocationMarkerFor(fileName, 'my'); + expect(textSpan).toEqual(boundedText); + + expect(definitions).toBeDefined(); + expect(definitions !.length).toBe(4); + + const refFileName = '/node_modules/@angular/common/common.d.ts'; + for (const def of definitions !) { + expect(def.fileName).toBe(refFileName); + expect(def.name).toBe('async'); + expect(def.kind).toBe('pipe'); + // Not asserting the textSpan of definition because it's external file + } }); - function localReference(code: string) { - addCode(code, fileName => { - const refResult = mockHost.getReferenceMarkers(fileName) !; - for (const name in refResult.references) { - const references = refResult.references[name]; - const definitions = refResult.definitions[name]; - expect(definitions).toBeDefined(); // If this fails the test data is wrong. - for (const reference of references) { - const definition = ngService.getDefinitionAt(fileName, reference.start); - if (definition) { - definition.forEach(d => expect(d.fileName).toEqual(fileName)); - const match = matchingSpan(definition.map(d => d.span), definitions); - if (!match) { - throw new Error( - `Expected one of ${stringifySpans(definition.map(d => d.span))} to match one of ${stringifySpans(definitions)}`); - } - } else { - throw new Error('Expected a definition'); - } - } - } - }); - } - - function reference(referencedFile: string, code: string): void; - function reference(referencedFile: string, span: Span, code: string): void; - function reference(referencedFile: string, definition: string, code: string): void; - function reference(referencedFile: string, p1?: any, p2?: any): void { - const code: string = p2 ? p2 : p1; - const definition: string = p2 ? p1 : undefined; - let span: Span = p2 && p1.start != null ? p1 : undefined; - if (definition && !span) { - const referencedFileMarkers = mockHost.getReferenceMarkers(referencedFile) !; - expect(referencedFileMarkers).toBeDefined(); // If this fails the test data is wrong. - const spans = referencedFileMarkers.definitions[definition]; - expect(spans).toBeDefined(); // If this fails the test data is wrong. - span = spans[0]; - } - addCode(code, fileName => { - const refResult = mockHost.getReferenceMarkers(fileName) !; - let tests = 0; - for (const name in refResult.references) { - const references = refResult.references[name]; - expect(reference).toBeDefined(); // If this fails the test data is wrong. - for (const reference of references) { - tests++; - const definition = ngService.getDefinitionAt(fileName, reference.start); - if (definition) { - definition.forEach(d => { - if (d.fileName.indexOf(referencedFile) < 0) { - throw new Error( - `Expected reference to file ${referencedFile}, received ${d.fileName}`); - } - if (span) { - expect(d.span).toEqual(span); - } - }); - } else { - throw new Error('Expected a definition'); - } - } - } - if (!tests) { - throw new Error('Expected at least one reference (test data error)'); - } - }); - } - - function addCode(code: string, cb: (fileName: string, content?: string) => void) { + /** + * Append a snippet of code to `app.component.ts` and return the file name. + * There must not be any name collision with existing code. + * @param code Snippet of code + */ + function addCode(code: string) { const fileName = '/app/app.component.ts'; const originalContent = mockHost.getFileContent(fileName); const newContent = originalContent + code; - mockHost.override(fileName, originalContent + code); - try { - cb(fileName, newContent); - } finally { - mockHost.override(fileName, undefined !); - } + mockHost.override(fileName, newContent); + return fileName; + } + + /** + * Returns the definition marker ᐱselectorᐱ for the specified 'selector'. + * Asserts that marker exists. + * @param fileName name of the file + * @param selector name of the marker + */ + function getDefinitionMarkerFor(fileName: string, selector: string): ts.TextSpan { + const markers = mockHost.getReferenceMarkers(fileName); + expect(markers).toBeDefined(); + expect(Object.keys(markers !.definitions)).toContain(selector); + expect(markers !.definitions[selector].length).toBe(1); + const marker = markers !.definitions[selector][0]; + expect(marker.start).toBeLessThanOrEqual(marker.end); + return { + start: marker.start, + length: marker.end - marker.start, + }; + } + + /** + * Returns the reference marker «selector» for the specified 'selector'. + * Asserts that marker exists. + * @param fileName name of the file + * @param selector name of the marker + */ + function getReferenceMarkerFor(fileName: string, selector: string): ts.TextSpan { + const markers = mockHost.getReferenceMarkers(fileName); + expect(markers).toBeDefined(); + expect(Object.keys(markers !.references)).toContain(selector); + expect(markers !.references[selector].length).toBe(1); + const marker = markers !.references[selector][0]; + expect(marker.start).toBeLessThanOrEqual(marker.end); + return { + start: marker.start, + length: marker.end - marker.start, + }; + } + + /** + * Returns the location marker ~{selector} for the specified 'selector'. + * Asserts that marker exists. + * @param fileName name of the file + * @param selector name of the marker + */ + function getLocationMarkerFor(fileName: string, selector: string): ts.TextSpan { + const markers = mockHost.getMarkerLocations(fileName); + expect(markers).toBeDefined(); + const start = markers ![`start-${selector}`]; + expect(start).toBeDefined(); + const end = markers ![`end-${selector}`]; + expect(end).toBeDefined(); + expect(start).toBeLessThanOrEqual(end); + return { + start: start, + length: end - start, + }; } }); - -function matchingSpan(aSpans: Span[], bSpans: Span[]): Span|undefined { - for (const a of aSpans) { - for (const b of bSpans) { - if (a.start == b.start && a.end == b.end) { - return a; - } - } - } -} - -function stringifySpan(span: Span) { - return span ? `(${span.start}-${span.end})` : ''; -} - -function stringifySpans(spans: Span[]) { - return spans ? `[${spans.map(stringifySpan).join(', ')}]` : ''; -} diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index 7e0d3ffdab..3cbc062dde 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -28,43 +28,43 @@ describe('hover', () => { it('should be able to find field in an interpolation', () => { hover( ` @Component({template: '{{«name»}}'}) export class MyComponent { name: string; }`, - 'property name of MyComponent'); + '(property) MyComponent.name'); }); it('should be able to find a field in a attribute reference', () => { hover( ` @Component({template: ''}) export class MyComponent { name: string; }`, - 'property name of MyComponent'); + '(property) MyComponent.name'); }); it('should be able to find a method from a call', () => { hover( ` @Component({template: '
'}) export class MyComponent { myClick() { }}`, - 'method myClick of MyComponent'); + '(method) MyComponent.myClick'); }); it('should be able to find a field reference in an *ngIf', () => { hover( ` @Component({template: '
'}) export class MyComponent { include = true;}`, - 'property include of MyComponent'); + '(property) MyComponent.include'); }); it('should be able to find a reference to a component', () => { hover( ` @Component({template: '«<ᐱtestᐱ-comp>
»'}) export class MyComponent { }`, - 'component TestComponent'); + '(component) TestComponent'); }); it('should be able to find an event provider', () => { hover( ` @Component({template: ''}) export class MyComponent { myHandler() {} }`, - 'event testEvent of TestComponent'); + '(event) TestComponent.testEvent'); }); it('should be able to find an input provider', () => { hover( ` @Component({template: ''}) export class MyComponent { name = 'my name'; }`, - 'property name of TestComponent'); + '(property) TestComponent.name'); }); it('should be able to ignore a reference declaration', () => { @@ -87,10 +87,13 @@ describe('hover', () => { []).concat(markers.definitions[referenceName] || []); for (const reference of references) { tests++; - const hover = ngService.getHoverAt(fileName, reference.start); - if (!hover) throw new Error(`Expected a hover at location ${reference.start}`); - expect(hover.span).toEqual(reference); - expect(toText(hover)).toEqual(hoverText); + const quickInfo = ngService.getHoverAt(fileName, reference.start); + if (!quickInfo) throw new Error(`Expected a hover at location ${reference.start}`); + expect(quickInfo.textSpan).toEqual({ + start: reference.start, + length: reference.end - reference.start, + }); + expect(toText(quickInfo)).toEqual(hoverText); } } expect(tests).toBeGreaterThan(0); // If this fails the test is wrong. @@ -109,5 +112,8 @@ describe('hover', () => { } } - function toText(hover: Hover): string { return hover.text.map(h => h.text).join(''); } + function toText(quickInfo: ts.QuickInfo): string { + const displayParts = quickInfo.displayParts || []; + return displayParts.map(p => p.text).join(''); + } }); diff --git a/packages/language-service/test/test_data.ts b/packages/language-service/test/test_data.ts index ebc639f9e2..9db91e508d 100644 --- a/packages/language-service/test/test_data.ts +++ b/packages/language-service/test/test_data.ts @@ -139,11 +139,11 @@ export class ForUsingComponent { @Component({template: '
{{~{test-comp-content}}} {{test1.~{test-comp-after-test}name}} {{div.~{test-comp-after-div}.innerText}}
'}) export class References {} -@Component({selector: 'test-comp', template: '
Testing: {{name}}
'}) +~{start-test-comp}@Component({selector: 'test-comp', template: '
Testing: {{name}}
'}) export class TestComponent { «@Input('ᐱtcNameᐱ') name = 'test';» «@Output('ᐱtestᐱ') testEvent = new EventEmitter();» -} +}~{end-test-comp} @Component({templateUrl: 'test.ng'}) export class TemplateReference {