2016-11-22 12:10:23 -05:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
|
|
|
import {createLanguageService} from '../src/language_service';
|
|
|
|
import {TypeScriptServiceHost} from '../src/typescript_host';
|
|
|
|
|
2017-01-03 20:21:45 -05:00
|
|
|
import {MockTypescriptHost} from './test_utils';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-17 21:42:27 -04:00
|
|
|
const TEST_TEMPLATE = '/app/test.ng';
|
|
|
|
|
2019-10-16 15:00:41 -04:00
|
|
|
describe('hover', () => {
|
2019-10-16 14:01:31 -04:00
|
|
|
const mockHost = new MockTypescriptHost(['/app/main.ts']);
|
|
|
|
const tsLS = ts.createLanguageService(mockHost);
|
|
|
|
const ngLSHost = new TypeScriptServiceHost(mockHost, tsLS);
|
|
|
|
const ngLS = createLanguageService(ngLSHost);
|
2019-08-28 15:55:12 -04:00
|
|
|
|
2019-10-16 15:00:41 -04:00
|
|
|
beforeEach(() => { mockHost.reset(); });
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
it('should be able to find field in an interpolation', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '{{«name»}}'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
name: string;
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2020-02-17 05:38:12 -05:00
|
|
|
it('should be able to find an interpolated value in an attribute', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div string-model model="{{«title»}}"></div>`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title');
|
|
|
|
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.title: string');
|
|
|
|
});
|
|
|
|
|
2016-11-22 12:10:23 -05:00
|
|
|
it('should be able to find a field in a attribute reference', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<input [(ngModel)]="«name»">'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
name: string;
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to find a method from a call', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<div (click)="«ᐱmyClickᐱ()»;"></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
myClick() { }
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'myClick');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
|
|
|
expect(textSpan.length).toBe('myClick()'.length);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(method) MyComponent.myClick: () => void');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to find a field reference in an *ngIf', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<div *ngIf="«include»"></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
include = true;
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'include');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.include: boolean');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to find a reference to a component', () => {
|
2020-01-23 14:34:27 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, '<~{cursor}test-comp></test-comp>');
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
|
|
|
expect(quickInfo).toBeDefined();
|
|
|
|
const {displayParts, documentation} = quickInfo !;
|
|
|
|
expect(toText(displayParts)).toBe('(component) AppModule.TestComponent: typeof TestComponent');
|
|
|
|
expect(toText(documentation)).toBe('This Component provides the `test-comp` selector.');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
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
2019-10-12 20:11:55 -04:00
|
|
|
it('should be able to find a reference to a directive', () => {
|
2020-01-23 14:34:27 -05:00
|
|
|
const content = mockHost.override(TEST_TEMPLATE, `<div string-model~{cursor}></div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
|
|
|
expect(quickInfo).toBeDefined();
|
|
|
|
const {displayParts, textSpan} = quickInfo !;
|
|
|
|
expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: typeof StringModel');
|
|
|
|
expect(content.substring(textSpan.start, textSpan.start + textSpan.length))
|
|
|
|
.toBe('string-model');
|
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
2019-10-12 20:11:55 -04:00
|
|
|
});
|
|
|
|
|
2016-11-22 12:10:23 -05:00
|
|
|
it('should be able to find an event provider', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<test-comp «(ᐱtestᐱ)="myHandler()"»></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
myHandler() {}
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'test');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(event) TestComponent.testEvent: EventEmitter<any>');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to find an input provider', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<test-comp «[ᐱtcNameᐱ]="name"»></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
name = 'my name';
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getDefinitionMarkerFor(fileName, 'tcName');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(property) TestComponent.name: string');
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
|
|
|
|
2020-01-17 05:11:23 -05:00
|
|
|
describe('over structural directive', () => {
|
|
|
|
it('should be able to find the directive', () => {
|
|
|
|
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);
|
|
|
|
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, `<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[]');
|
|
|
|
});
|
2019-12-26 02:19:38 -05:00
|
|
|
});
|
|
|
|
|
2020-01-03 05:32:30 -05:00
|
|
|
it('should be able to find a reference to a two-way binding', () => {
|
2020-01-03 22:30:14 -05:00
|
|
|
mockHost.override(TEST_TEMPLATE, `<test-comp string-model «[(ᐱmodelᐱ)]="title"»></test-comp>`);
|
|
|
|
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'model');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
2020-01-03 05:32:30 -05:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
|
|
|
expect(toText(displayParts)).toBe('(property) StringModel.model: string');
|
|
|
|
});
|
|
|
|
|
2017-07-07 11:46:18 -04:00
|
|
|
it('should be able to ignore a reference declaration', () => {
|
2019-08-28 15:55:12 -04:00
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<div #«chart»></div>'
|
|
|
|
})
|
|
|
|
export class MyComponent { }`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'chart');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-08-28 15:55:12 -04:00
|
|
|
expect(quickInfo).toBeUndefined();
|
2017-07-07 11:46:18 -04:00
|
|
|
});
|
2019-09-16 22:07:43 -04:00
|
|
|
|
2019-10-11 20:15:07 -04:00
|
|
|
it('should be able to find the NgModule of a component', () => {
|
2019-09-16 22:07:43 -04:00
|
|
|
const fileName = '/app/app.component.ts';
|
|
|
|
mockHost.override(fileName, `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div></div>'
|
|
|
|
})
|
|
|
|
export class «AppComponent» {
|
|
|
|
name: string;
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'AppComponent');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-09-16 22:07:43 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
2019-10-11 20:15:07 -04:00
|
|
|
expect(toText(displayParts)).toBe('(component) AppModule.AppComponent: class');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should be able to find the NgModule of a directive', () => {
|
2019-10-16 15:00:41 -04:00
|
|
|
const fileName = '/app/parsing-cases.ts';
|
|
|
|
const content = mockHost.readFile(fileName) !;
|
|
|
|
const position = content.indexOf('StringModel');
|
|
|
|
expect(position).toBeGreaterThan(0);
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, position);
|
2019-10-11 20:15:07 -04:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
2019-10-16 15:00:41 -04:00
|
|
|
expect(textSpan).toEqual({
|
|
|
|
start: position,
|
|
|
|
length: 'StringModel'.length,
|
|
|
|
});
|
|
|
|
expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class');
|
2019-09-16 22:07:43 -04:00
|
|
|
});
|
2019-10-17 21:42:27 -04:00
|
|
|
|
|
|
|
it('should be able to provide quick info for $any() cast function', () => {
|
|
|
|
const content = mockHost.override(TEST_TEMPLATE, '<div>{{$any(title)}}</div>');
|
|
|
|
const position = content.indexOf('$any');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, position);
|
2019-10-17 21:42:27 -04:00
|
|
|
expect(quickInfo).toBeDefined();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual({
|
|
|
|
start: position,
|
|
|
|
length: '$any(title)'.length,
|
|
|
|
});
|
2019-12-20 13:26:24 -05:00
|
|
|
expect(toText(displayParts)).toBe('(method) $any: $any');
|
2019-10-17 21:42:27 -04:00
|
|
|
});
|
2019-12-19 21:33:26 -05:00
|
|
|
|
|
|
|
it('should provide documentation for a property', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<div>{{~{cursor}title}}</div>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
2019-12-19 21:33:26 -05:00
|
|
|
expect(quickInfo).toBeDefined();
|
|
|
|
const documentation = toText(quickInfo !.documentation);
|
|
|
|
expect(documentation).toBe('This is the title of the `TemplateReference` Component.');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should provide documentation for a selector', () => {
|
|
|
|
mockHost.override(TEST_TEMPLATE, `<~{cursor}test-comp></test-comp>`);
|
|
|
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
2019-12-19 21:33:26 -05:00
|
|
|
expect(quickInfo).toBeDefined();
|
|
|
|
const documentation = toText(quickInfo !.documentation);
|
|
|
|
expect(documentation).toBe('This Component provides the `test-comp` selector.');
|
|
|
|
});
|
2019-12-21 20:23:09 -05:00
|
|
|
|
|
|
|
it('should not expand i18n templates', () => {
|
|
|
|
const fileName = mockHost.addCode(`
|
|
|
|
@Component({
|
|
|
|
template: '<div i18n="@@el">{{«name»}}</div>'
|
|
|
|
})
|
|
|
|
export class MyComponent {
|
|
|
|
name: string;
|
|
|
|
}`);
|
|
|
|
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
|
2020-01-21 17:51:43 -05:00
|
|
|
const quickInfo = ngLS.getQuickInfoAtPosition(fileName, marker.start);
|
2019-12-21 20:23:09 -05:00
|
|
|
expect(quickInfo).toBeTruthy();
|
|
|
|
const {textSpan, displayParts} = quickInfo !;
|
|
|
|
expect(textSpan).toEqual(marker);
|
|
|
|
expect(toText(displayParts)).toBe('(property) MyComponent.name: string');
|
|
|
|
});
|
2019-05-08 13:30:54 -04:00
|
|
|
});
|
2019-08-28 15:55:12 -04:00
|
|
|
|
|
|
|
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
|
|
|
return (displayParts || []).map(p => p.text).join('');
|
|
|
|
}
|