Rather than mutating the span on the template when renaming literal strings, this commit updates the logic to mutate the `TextSpan` equivalent that is used by the Language Service. PR Close #40484
		
			
				
	
	
		
			1477 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1477 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC 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 {absoluteFrom, absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system';
 | |
| import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
 | |
| 
 | |
| import {extractCursorInfo, LanguageServiceTestEnvironment} from './env';
 | |
| import {assertFileNames, assertTextSpans, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils';
 | |
| 
 | |
| describe('find references and rename locations', () => {
 | |
|   let env: LanguageServiceTestEnvironment;
 | |
| 
 | |
|   beforeEach(() => {
 | |
|     initMockFileSystem('Native');
 | |
|   });
 | |
| 
 | |
|   afterEach(() => {
 | |
|     // Clear env so it's not accidentally carried over to the next test.
 | |
|     env = undefined!;
 | |
|   });
 | |
| 
 | |
|   describe('cursor is on binding in component class', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './app.html'})
 | |
|           export class AppCmp {
 | |
|             myP¦rop!: string;
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|       const templateFile = {name: _('/app.html'), contents: '{{myProp}}'};
 | |
|       env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets component member references from TS file and external template', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
|       assertFileNames(refs, ['app.html', 'app.ts']);
 | |
|       assertTextSpans(refs, ['myProp']);
 | |
|     });
 | |
| 
 | |
|     it('gets rename locations from TS file and external template', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
|       assertFileNames(renameLocations, ['app.html', 'app.ts']);
 | |
|       assertTextSpans(renameLocations, ['myProp']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor is on binding in an external template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './app.html'})
 | |
|           export class AppCmp {
 | |
|             myProp = '';
 | |
|           }`,
 | |
|       };
 | |
|       const cursorInfo = extractCursorInfo('{{myP¦rop}}');
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets references', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
|       assertFileNames(refs, ['app.html', 'app.ts']);
 | |
|       assertTextSpans(refs, ['myProp']);
 | |
|     });
 | |
| 
 | |
|     it('gets rename locations', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
|       assertFileNames(renameLocations, ['app.html', 'app.ts']);
 | |
|       assertTextSpans(renameLocations, ['myProp']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor is on function call in external template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '<div (click)="set¦Title(2)"></div>'})
 | |
|           export class AppCmp {
 | |
|             setTitle(s: number) {}
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets component member reference in ts file', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(refs, ['app.ts']);
 | |
|       assertTextSpans(refs, ['setTitle']);
 | |
|     });
 | |
| 
 | |
|     it('gets rename location in ts file', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(renameLocations, ['app.ts']);
 | |
|       assertTextSpans(renameLocations, ['setTitle']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on argument to a function call in an external template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '<div (click)="setTitle(ti¦tle)"></div>'})
 | |
|           export class AppCmp {
 | |
|             title = '';
 | |
|             setTitle(s: string) {}
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets member reference in ts file', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertTextSpans(refs, ['title']);
 | |
|     });
 | |
| 
 | |
|     it('finds rename location in ts file', () => {
 | |
|       const refs = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertTextSpans(refs, ['title']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor is on $event in method call arguments', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
|           @Component({template: '<div (click)="setTitle($even¦t)"></div>'})
 | |
|           export class AppCmp {
 | |
|             setTitle(s: any) {}
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('find references', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(1);
 | |
| 
 | |
|       assertTextSpans(refs, ['$event']);
 | |
|     });
 | |
| 
 | |
|     it('gets no rename locations', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(renameLocations).toBeUndefined();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on LHS of property write in external template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './app.html' })
 | |
|           export class AppCmp {
 | |
|             title = '';
 | |
|           }`,
 | |
|       };
 | |
|       const templateFileWithCursor = `<div (click)="ti¦tle = 'newtitle'"></div>`;
 | |
|       const cursorInfo = extractCursorInfo(templateFileWithCursor);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets member reference in ts file', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(refs, ['app.ts', 'app.html']);
 | |
|       assertTextSpans(refs, ['title']);
 | |
|     });
 | |
| 
 | |
|     it('gets rename location in ts file', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(renameLocations, ['app.ts', 'app.html']);
 | |
|       assertTextSpans(renameLocations, ['title']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on RHS of property write in external template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '<div (click)="title = otherT¦itle"></div>' })
 | |
|           export class AppCmp {
 | |
|             title = '';
 | |
|             otherTitle = '';
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: cursorInfo.text,
 | |
|       };
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('get reference to member in ts file', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(refs, ['app.ts']);
 | |
|       assertTextSpans(refs, ['otherTitle']);
 | |
|     });
 | |
| 
 | |
|     it('finds rename location in ts file', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(renameLocations, ['app.ts']);
 | |
|       assertTextSpans(renameLocations, ['otherTitle']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on a keyed read', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '{{hero["na¦me"]}}' })
 | |
|           export class AppCmp {
 | |
|             hero: {name: string} = {name: 'Superman'};
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: cursorInfo.text,
 | |
|       };
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets reference to member type definition and initialization in component class', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|       // 3 references: the type definition, the value assignment, and the read in the template
 | |
|       expect(refs.length).toBe(3);
 | |
| 
 | |
|       assertFileNames(refs, ['app.ts']);
 | |
|       // TODO(atscott): investigate if we can make the template keyed read be just the 'name' part.
 | |
|       // The TypeScript implementation specifically adjusts the span to accommodate string literals:
 | |
|       // https://sourcegraph.com/github.com/microsoft/TypeScript@d5779c75d3dd19565b60b9e2960b8aac36d4d635/-/blob/src/services/findAllReferences.ts#L508-512
 | |
|       // One possible solution would be to extend `FullTemplateMapping` to include the matched TCB
 | |
|       // node and then do the same thing that TS does: if the node is a string, adjust the span.
 | |
|       assertTextSpans(refs, ['name', '"name"']);
 | |
|     });
 | |
| 
 | |
|     it('gets rename locations in component class', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|       expect(renameLocations).toBeUndefined();
 | |
| 
 | |
|       // TODO(atscott): We should handle this case. The fix requires us to fix the result span as
 | |
|       // described above.
 | |
|       // 3 references: the type definition, the value assignment, and the read in the template
 | |
|       // expect(renameLocations.length).toBe(3);
 | |
|       //
 | |
|       // assertFileNames(renameLocations, ['app.ts']);
 | |
|       // assertTextSpans(renameLocations, ['name']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on RHS of keyed write in a template', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './app.html' })
 | |
|           export class AppCmp {
 | |
|             hero: {name: string} = {name: 'Superman'};
 | |
|             batman = 'batman';
 | |
|           }`,
 | |
|       };
 | |
|       const templateFileWithCursor = `<div (click)="hero['name'] = bat¦man"></div>`;
 | |
|       const cursorInfo = extractCursorInfo(templateFileWithCursor);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|     });
 | |
| 
 | |
|     it('get references in ts file', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(refs, ['app.ts', 'app.html']);
 | |
|       assertTextSpans(refs, ['batman']);
 | |
|     });
 | |
| 
 | |
|     it('finds rename location in ts file', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
| 
 | |
|       assertFileNames(renameLocations, ['app.ts', 'app.html']);
 | |
|       assertTextSpans(renameLocations, ['batman']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on an element reference', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '<input #myInput /> {{ myIn¦put.value }}'})
 | |
|           export class AppCmp {
 | |
|             title = '';
 | |
|           }`);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile]);
 | |
|     });
 | |
| 
 | |
|     it('get reference to declaration in template', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
| 
 | |
|       expect(refs.length).toBe(2);
 | |
|       assertTextSpans(refs, ['myInput']);
 | |
| 
 | |
|       // Get the declaration by finding the reference that appears first in the template
 | |
|       refs.sort((a, b) => a.textSpan.start - b.textSpan.start);
 | |
|       expect(refs[0].isDefinition).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('finds rename location in template', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
| 
 | |
|       expect(renameLocations.length).toBe(2);
 | |
|       assertTextSpans(renameLocations, ['myInput']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when cursor in on a template reference', () => {
 | |
|     let cursor: number;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       const templateWithCursor = `
 | |
|               <ng-template #myTemplate >bla</ng-template>
 | |
|               <ng-container [ngTemplateOutlet]="myTem¦plate"></ng-container>`;
 | |
|       const appFile = {
 | |
|         name: _('/app.ts'),
 | |
|         contents: `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './app.html'})
 | |
|           export class AppCmp {
 | |
|             title = '';
 | |
|           }`,
 | |
|       };
 | |
|       const cursorInfo = extractCursorInfo(templateWithCursor);
 | |
|       cursor = cursorInfo.cursor;
 | |
|       const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|       env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|     });
 | |
| 
 | |
|     it('gets reference to declaration', () => {
 | |
|       const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(refs.length).toBe(2);
 | |
|       assertTextSpans(refs, ['myTemplate']);
 | |
|       assertFileNames(refs, ['app.html']);
 | |
| 
 | |
|       // Get the declaration by finding the reference that appears first in the template
 | |
|       refs.sort((a, b) => a.textSpan.start - b.textSpan.start);
 | |
|       expect(refs[0].isDefinition).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('finds rename location in template', () => {
 | |
|       const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|       expect(renameLocations.length).toBe(2);
 | |
|       assertTextSpans(renameLocations, ['myTemplate']);
 | |
|       assertFileNames(renameLocations, ['app.html']);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('template references', () => {
 | |
|     describe('directives', () => {
 | |
|       let appFile: TestFile;
 | |
|       let dirFile: TestFile;
 | |
| 
 | |
|       beforeEach(() => {
 | |
|         const dirFileContents = `
 | |
|             import {Directive} from '@angular/core';
 | |
| 
 | |
|             @Directive({selector: '[dir]', exportAs: 'myDir'})
 | |
|             export class Dir {
 | |
|               dirValue!: string;
 | |
|               doSomething() {}
 | |
|             }`;
 | |
|         const appFileContents = `
 | |
|             import {Component} from '@angular/core';
 | |
| 
 | |
|             @Component({templateUrl: './app.html'})
 | |
|             export class AppCmp {}`;
 | |
|         appFile = {name: _('/app.ts'), contents: appFileContents};
 | |
|         dirFile = {name: _('/dir.ts'), contents: dirFileContents};
 | |
|       });
 | |
| 
 | |
|       describe('when cursor is on usage of template reference', () => {
 | |
|         let cursor: number;
 | |
|         beforeEach(() => {
 | |
|           const templateWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirR¦ef }}';
 | |
|           const cursorInfo = extractCursorInfo(templateWithCursor);
 | |
|           cursor = cursorInfo.cursor;
 | |
|           const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|           env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
 | |
|         });
 | |
| 
 | |
|         it('should get references', () => {
 | |
|           const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(refs.length).toBe(2);
 | |
|           assertFileNames(refs, ['app.html']);
 | |
|           assertTextSpans(refs, ['dirRef']);
 | |
|         });
 | |
| 
 | |
|         it('should find rename locations', () => {
 | |
|           const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(renameLocations.length).toBe(2);
 | |
|           assertFileNames(renameLocations, ['app.html']);
 | |
|           assertTextSpans(renameLocations, ['dirRef']);
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       describe('when cursor is on a property read of directive reference', () => {
 | |
|         let cursor: number;
 | |
|         beforeEach(() => {
 | |
|           const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef.dirV¦alue }}';
 | |
|           const cursorInfo = extractCursorInfo(fileWithCursor);
 | |
|           cursor = cursorInfo.cursor;
 | |
|           const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|           env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
 | |
|         });
 | |
| 
 | |
|         it('should get references', () => {
 | |
|           const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(refs.length).toBe(2);
 | |
|           assertFileNames(refs, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(refs, ['dirValue']);
 | |
|         });
 | |
| 
 | |
|         it('should find rename locations', () => {
 | |
|           const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(renameLocations.length).toBe(2);
 | |
|           assertFileNames(renameLocations, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(renameLocations, ['dirValue']);
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       describe('when cursor is on a safe prop read', () => {
 | |
|         let cursor: number;
 | |
|         beforeEach(() => {
 | |
|           const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef?.dirV¦alue }}';
 | |
|           const cursorInfo = extractCursorInfo(fileWithCursor);
 | |
|           cursor = cursorInfo.cursor;
 | |
|           const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|           env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
 | |
|         });
 | |
| 
 | |
| 
 | |
|         it('should get references', () => {
 | |
|           const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(refs.length).toBe(2);
 | |
|           assertFileNames(refs, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(refs, ['dirValue']);
 | |
|         });
 | |
| 
 | |
|         it('should find rename locations', () => {
 | |
|           const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(renameLocations.length).toBe(2);
 | |
|           assertFileNames(renameLocations, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(renameLocations, ['dirValue']);
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       describe('when cursor is on safe method call', () => {
 | |
|         let cursor: number;
 | |
|         beforeEach(() => {
 | |
|           const fileWithCursor = '<div [dir] #dirRef="myDir"></div> {{ dirRef?.doSometh¦ing() }}';
 | |
|           const cursorInfo = extractCursorInfo(fileWithCursor);
 | |
|           cursor = cursorInfo.cursor;
 | |
|           const templateFile = {name: _('/app.html'), contents: cursorInfo.text};
 | |
|           env = createModuleWithDeclarations([appFile, dirFile], [templateFile]);
 | |
|         });
 | |
| 
 | |
| 
 | |
|         it('should get references', () => {
 | |
|           const refs = getReferencesAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(refs.length).toBe(2);
 | |
|           assertFileNames(refs, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(refs, ['doSomething']);
 | |
|         });
 | |
| 
 | |
|         it('should find rename locations', () => {
 | |
|           const renameLocations = getRenameLocationsAtPosition(_('/app.html'), cursor)!;
 | |
|           expect(renameLocations.length).toBe(2);
 | |
|           assertFileNames(renameLocations, ['dir.ts', 'app.html']);
 | |
|           assertTextSpans(renameLocations, ['doSomething']);
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('template variables', () => {
 | |
|     describe('when cursor is on variable which was initialized implicitly', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|           <div *ngFor="let hero of heroes">
 | |
|             <span *ngIf="hero">
 | |
|               {{her¦o}}
 | |
|             </span>
 | |
|           </div>
 | |
|           `);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {
 | |
|           name: _('/app.ts'),
 | |
|           contents: `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({templateUrl: './template.ng.html'})
 | |
|           export class AppCmp {
 | |
|             heroes: string[] = [];
 | |
|           }`
 | |
|         };
 | |
|         const templateFile = {name: _('/template.ng.html'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile], [templateFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/template.ng.html'), cursor)!;
 | |
|         expect(refs.length).toBe(3);
 | |
|         assertFileNames(refs, ['template.ng.html']);
 | |
|         assertTextSpans(refs, ['hero']);
 | |
| 
 | |
|         const originalRefs = env.ngLS.getReferencesAtPosition(_('/template.ng.html'), cursor)!;
 | |
|         // Get the declaration by finding the reference that appears first in the template
 | |
|         originalRefs.sort((a, b) => a.textSpan.start - b.textSpan.start);
 | |
|         expect(originalRefs[0].isDefinition).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/template.ng.html'), cursor)!;
 | |
|         expect(renameLocations.length).toBe(3);
 | |
|         assertFileNames(renameLocations, ['template.ng.html']);
 | |
|         assertTextSpans(renameLocations, ['hero']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on renamed variable', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({template: '<div *ngFor="let hero of heroes; let iRef = index">{{iR¦ef}}</div>'})
 | |
|           export class AppCmp {
 | |
|             heroes: string[] = [];
 | |
|           }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(2);
 | |
|         assertFileNames(refs, ['app.ts']);
 | |
|         assertTextSpans(refs, ['iRef']);
 | |
| 
 | |
|         const originalRefs = env.ngLS.getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         // Get the declaration by finding the reference that appears first in the template
 | |
|         originalRefs.sort((a, b) => a.textSpan.start - b.textSpan.start);
 | |
|         expect(originalRefs[0].isDefinition).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toBe(2);
 | |
|         assertFileNames(renameLocations, ['app.ts']);
 | |
|         assertTextSpans(renameLocations, ['iRef']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on initializer of variable', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const dirFile = `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
| 
 | |
|         export class ExampleContext<T> {
 | |
|           constructor(readonly $implicit: T, readonly identifier: string) {}
 | |
|         }
 | |
| 
 | |
|         @Directive({ selector: '[example]' })
 | |
|         export class ExampleDirective<T> {
 | |
|           @Input() set example(v: T) { }
 | |
|           static ngTemplateContextGuard<T>(dir: ExampleDirective<T>, ctx: unknown):
 | |
|             ctx is ExampleContext<T> {
 | |
|             return true;
 | |
|           }
 | |
|         }`;
 | |
|         const fileWithCursor = `
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {ExampleDirective} from './example-directive';
 | |
| 
 | |
|         @Component({template: '<div *example="state; let id = identif¦ier">{{id}}</div>'})
 | |
|         export class AppCmp {
 | |
|           state = {};
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, ExampleDirective]})
 | |
|         export class AppModule {}`;
 | |
|         const cursorInfo = extractCursorInfo(fileWithCursor);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: cursorInfo.text, isRoot: true},
 | |
|           {name: _('/example-directive.ts'), contents: dirFile},
 | |
|         ]);
 | |
|         env.expectNoSourceDiagnostics();
 | |
|         env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp');
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(2);
 | |
|         assertFileNames(refs, ['app.ts', 'example-directive.ts']);
 | |
|         assertTextSpans(refs, ['identifier']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toBe(2);
 | |
|         assertFileNames(renameLocations, ['app.ts', 'example-directive.ts']);
 | |
|         assertTextSpans(renameLocations, ['identifier']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on property read of variable', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|             import {Component} from '@angular/core';
 | |
| 
 | |
|             @Component({template: '<div *ngFor="let hero of heroes">{{hero.na¦me}}</div>'})
 | |
|             export class AppCmp {
 | |
|               heroes: Array<{name: string}> = [];
 | |
|             }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(2);
 | |
|         assertFileNames(refs, ['app.ts']);
 | |
|         assertTextSpans(refs, ['name']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toBe(2);
 | |
|         assertFileNames(renameLocations, ['app.ts']);
 | |
|         assertTextSpans(renameLocations, ['name']);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('pipes', () => {
 | |
|     let prefixPipeFile: TestFile;
 | |
|     beforeEach(() => {
 | |
|       const prefixPipe = `
 | |
|         import {Pipe, PipeTransform} from '@angular/core';
 | |
| 
 | |
|         @Pipe({ name: 'prefixPipe' })
 | |
|         export class PrefixPipe implements PipeTransform {
 | |
|           transform(value: string, prefix: string): string;
 | |
|           transform(value: number, prefix: number): number;
 | |
|           transform(value: string|number, prefix: string|number): string|number {
 | |
|             return '';
 | |
|           }
 | |
|         }`;
 | |
|       prefixPipeFile = {name: _('/prefix-pipe.ts'), contents: prefixPipe};
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on pipe name', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const appContentsWithCursor = `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '{{birthday | prefi¦xPipe: "MM/dd/yy"}}'})
 | |
|         export class AppCmp {
 | |
|           birthday = '';
 | |
|         }
 | |
|       `;
 | |
|         const cursorInfo = extractCursorInfo(appContentsWithCursor);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, prefixPipeFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(5);
 | |
|         assertFileNames(refs, ['index.d.ts', 'prefix-pipe.ts', 'app.ts']);
 | |
|         assertTextSpans(refs, ['transform', 'prefixPipe']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
| 
 | |
|         // TODO(atscott): Add support for renaming the pipe 'name'
 | |
|         // expect(renameLocations.length).toBe(2);
 | |
|         // assertFileNames(renameLocations, ['prefix-pipe.ts', 'app.ts']);
 | |
|         // assertTextSpans(renameLocations, ['prefixPipe']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on pipe argument', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const appContentsWithCursor = `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '{{birthday | prefixPipe: pr¦efix}}'})
 | |
|         export class AppCmp {
 | |
|           birthday = '';
 | |
|           prefix = '';
 | |
|         }
 | |
|       `;
 | |
|         const cursorInfo = extractCursorInfo(appContentsWithCursor);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, prefixPipeFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(2);
 | |
|         assertFileNames(refs, ['app.ts']);
 | |
|         assertTextSpans(refs, ['prefix']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toBe(2);
 | |
|         assertFileNames(renameLocations, ['app.ts']);
 | |
|         assertTextSpans(renameLocations, ['prefix']);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('inputs', () => {
 | |
|     const dirFileContents = `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
| 
 | |
|         @Directive({selector: '[string-model]'})
 | |
|         export class StringModel {
 | |
|           @Input() model!: string;
 | |
|           @Input('alias') aliasedModel!: string;
 | |
|         }`;
 | |
|     describe('when cursor is on the input in the template', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const stringModelTestFile = {name: _('/string-model.ts'), contents: dirFileContents};
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model [mod¦el]="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertFileNames(refs, ['string-model.ts', 'app.ts']);
 | |
|         assertTextSpans(refs, ['model']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toEqual(2);
 | |
|         assertFileNames(renameLocations, ['string-model.ts', 'app.ts']);
 | |
|         assertTextSpans(renameLocations, ['model']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on an input that maps to multiple directives', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const otherDirFile = {
 | |
|           name: _('/other-dir.ts'),
 | |
|           contents: `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
| 
 | |
|         @Directive({selector: '[string-model]'})
 | |
|         export class OtherDir {
 | |
|           @Input('model') model!: any;
 | |
|         }
 | |
|         `
 | |
|         };
 | |
|         const stringModelTestFile = {name: _('/string-model.ts'), contents: dirFileContents};
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model [mod¦el]="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]);
 | |
|       });
 | |
| 
 | |
|       // TODO(atscott): This test does not pass because the template symbol builder only returns one
 | |
|       // binding.
 | |
|       xit('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(3);
 | |
|         assertFileNames(refs, ['string-model.ts', 'app.ts', 'other-dir']);
 | |
|         assertTextSpans(refs, ['model', 'otherDirAliasedInput']);
 | |
|       });
 | |
| 
 | |
|       // TODO(atscott): This test fails because template symbol builder only returns one binding.
 | |
|       // The result is that rather than returning `undefined` because we don't handle alias inputs,
 | |
|       // we return the rename locations for the first binding.
 | |
|       xit('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott):
 | |
|         // expect(renameLocations.length).toEqual(3);
 | |
|         // assertFileNames(renameLocations, ['string-model.ts', 'app.ts', 'other-dir']);
 | |
|         // assertTextSpans(renameLocations, ['model']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('should work when cursor is on text attribute input', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const stringModelTestFile = {name: _('/string-model.ts'), contents: dirFileContents};
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model mod¦el="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile]);
 | |
|       });
 | |
| 
 | |
|       it('should work for text attributes', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertFileNames(refs, ['string-model.ts', 'app.ts']);
 | |
|         assertTextSpans(refs, ['model']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toEqual(2);
 | |
|         assertFileNames(renameLocations, ['string-model.ts', 'app.ts']);
 | |
|         assertTextSpans(renameLocations, ['model']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on the class member input', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const dirFileWithCursor = `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
| 
 | |
|         @Directive({selector: '[string-model]'})
 | |
|         export class StringModel {
 | |
|           @Input() mod¦el!: string;
 | |
|         }`;
 | |
|         const cursorInfo = extractCursorInfo(dirFileWithCursor);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const stringModelTestFile = {name: _('/string-model.ts'), contents: cursorInfo.text};
 | |
|         const appFile = {
 | |
|           name: _('/app.ts'),
 | |
|           contents: `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model model="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`,
 | |
|         };
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile]);
 | |
|       });
 | |
| 
 | |
|       it('should work from the TS input declaration', () => {
 | |
|         const refs = getReferencesAtPosition(_('/string-model.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertFileNames(refs, ['app.ts', 'string-model.ts']);
 | |
|         assertTextSpans(refs, ['model']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/string-model.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toEqual(2);
 | |
|         assertFileNames(renameLocations, ['app.ts', 'string-model.ts']);
 | |
|         assertTextSpans(renameLocations, ['model']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on input referenced somewhere in the class functions', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const otherDirContents = `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
|         import {StringModel} from './string-model';
 | |
| 
 | |
|         @Directive({selector: '[other-dir]'})
 | |
|         export class OtherDir {
 | |
|           @Input() stringModelRef!: StringModel;
 | |
| 
 | |
|           doSomething() {
 | |
|             console.log(this.stringModelRef.mod¦el);
 | |
|           }
 | |
|         }`;
 | |
|         const cursorInfo = extractCursorInfo(otherDirContents);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const otherDirFile = {name: _('/other-dir.ts'), contents: cursorInfo.text};
 | |
|         const stringModelTestFile = {
 | |
|           name: _('/string-model.ts'),
 | |
|           contents: `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
| 
 | |
|         @Directive({selector: '[string-model]'})
 | |
|         export class StringModel {
 | |
|           @Input() model!: string;
 | |
|         }`,
 | |
|         };
 | |
|         const appFile = {
 | |
|           name: _('/app.ts'),
 | |
|           contents: `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model other-dir model="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`,
 | |
|         };
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/other-dir.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(3);
 | |
|         assertFileNames(refs, ['app.ts', 'string-model.ts', 'other-dir.ts']);
 | |
|         assertTextSpans(refs, ['model']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/other-dir.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toEqual(3);
 | |
|         assertFileNames(renameLocations, ['app.ts', 'string-model.ts', 'other-dir.ts']);
 | |
|         assertTextSpans(renameLocations, ['model']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on an aliased input', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const stringModelTestFile = {name: _('/string-model.ts'), contents: dirFileContents};
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div string-model [al¦ias]="title"></div>'})
 | |
|         export class AppCmp {
 | |
|           title = 'title';
 | |
|         }`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile, stringModelTestFile]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertFileNames(refs, ['string-model.ts', 'app.ts']);
 | |
|         assertTextSpans(refs, ['aliasedModel', 'alias']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): add support for renaming alias outputs
 | |
|         // expect(renameLocations.length).toEqual(2);
 | |
|         // assertFileNames(renameLocations, ['string-model.ts', 'app.ts']);
 | |
|         // assertTextSpans(renameLocations, ['alias']);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('outputs', () => {
 | |
|     const dirFile = `
 | |
|         import {Directive, Output, EventEmitter} from '@angular/core';
 | |
| 
 | |
|         @Directive({selector: '[string-model]'})
 | |
|         export class StringModel {
 | |
|           @Output() modelChange = new EventEmitter<string>();
 | |
|           @Output('alias') aliasedModelChange = new EventEmitter<string>();
 | |
|         }`;
 | |
| 
 | |
|     function generateAppFile(template: string) {
 | |
|       return `
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {StringModel} from './string-model';
 | |
| 
 | |
|         @Component({template: '${template}'})
 | |
|         export class AppCmp {
 | |
|           setTitle(s: string) {}
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, StringModel]})
 | |
|         export class AppModule {}`;
 | |
|     }
 | |
| 
 | |
|     describe('when cursor is on output key in template', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(
 | |
|             generateAppFile(`<div string-model (mod¦elChange)="setTitle($event)"></div>`));
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: cursorInfo.text, isRoot: true},
 | |
|           {name: _('/string-model.ts'), contents: dirFile},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertTextSpans(refs, ['modelChange']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations.length).toEqual(2);
 | |
|         assertTextSpans(renameLocations, ['modelChange']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on alias output key', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(
 | |
|             generateAppFile(`<div string-model (a¦lias)="setTitle($event)"></div>`));
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: cursorInfo.text, isRoot: true},
 | |
|           {name: _('/string-model.ts'), contents: dirFile},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toEqual(2);
 | |
|         assertTextSpans(refs, ['aliasedModelChange', 'alias']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): add support for renaming alias outputs
 | |
|         // expect(renameLocations.length).toEqual(2);
 | |
|         // assertTextSpans(renameLocations, ['alias']);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('should get references to both input and output for two-way binding', () => {
 | |
|     const dirFile = {
 | |
|       name: _('/dir.ts'),
 | |
|       contents: `
 | |
|       import {Directive, Input, Output} from '@angular/core';
 | |
| 
 | |
|       @Directive({selector: '[string-model]'})
 | |
|       export class StringModel {
 | |
|         @Input() model!: any;
 | |
|         @Output() modelChange!: any;
 | |
|       }`
 | |
|     };
 | |
|     const {text, cursor} = extractCursorInfo(`
 | |
|     import {Component} from '@angular/core';
 | |
| 
 | |
|     @Component({template: '<div string-model [(mod¦el)]="title"></div>'})
 | |
|     export class AppCmp {
 | |
|       title = 'title';
 | |
|     }`);
 | |
|     const appFile = {name: _('/app.ts'), contents: text};
 | |
|     env = createModuleWithDeclarations([appFile, dirFile]);
 | |
| 
 | |
|     const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|     // Note that this includes the 'model` twice from the template. As with other potential
 | |
|     // duplicates (like if another plugin returns the same span), we expect the LS clients to filter
 | |
|     // these out themselves.
 | |
|     expect(refs.length).toEqual(4);
 | |
|     assertFileNames(refs, ['dir.ts', 'app.ts']);
 | |
|     assertTextSpans(refs, ['model', 'modelChange']);
 | |
|   });
 | |
| 
 | |
|   describe('directives', () => {
 | |
|     describe('when cursor is on the directive class', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|       import {Directive} from '@angular/core';
 | |
| 
 | |
|       @Directive({selector: '[dir]'})
 | |
|       export class Di¦r {}`);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = `
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {Dir} from './dir';
 | |
| 
 | |
|         @Component({template: '<div dir></div>'})
 | |
|         export class AppCmp {
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, Dir]})
 | |
|         export class AppModule {}
 | |
|       `;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: appFile, isRoot: true},
 | |
|           {name: _('/dir.ts'), contents: cursorInfo.text},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should find references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/dir.ts'), cursor)!;
 | |
|         // 4 references are:  class declaration, template usage, app import and use in declarations
 | |
|         // list.
 | |
|         expect(refs.length).toBe(4);
 | |
|         assertTextSpans(refs, ['<div dir>', 'Dir']);
 | |
|         assertFileNames(refs, ['app.ts', 'dir.ts']);
 | |
|       });
 | |
| 
 | |
|       it('should find rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/dir.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): We should handle this case, but exclude the template results
 | |
|         // expect(renameLocations.length).toBe(3);
 | |
|         // assertTextSpans(renameLocations, ['Dir']);
 | |
|         // assertFileNames(renameLocations, ['app.ts', 'dir.ts']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on an attribute', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const dirFile = `
 | |
|       import {Directive} from '@angular/core';
 | |
| 
 | |
|       @Directive({selector: '[dir]'})
 | |
|       export class Dir {}`;
 | |
|         const dirFile2 = `
 | |
|       import {Directive} from '@angular/core';
 | |
| 
 | |
|       @Directive({selector: '[dir]'})
 | |
|       export class Dir2 {}`;
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {Dir} from './dir';
 | |
|         import {Dir2} from './dir2';
 | |
| 
 | |
|         @Component({template: '<div di¦r></div>'})
 | |
|         export class AppCmp {
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, Dir, Dir2]})
 | |
|         export class AppModule {}
 | |
|       `);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: cursorInfo.text, isRoot: true},
 | |
|           {name: _('/dir.ts'), contents: dirFile},
 | |
|           {name: _('/dir2.ts'), contents: dirFile2},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('gets references to all matching directives', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(8);
 | |
|         assertTextSpans(refs, ['<div dir>', 'Dir', 'Dir2']);
 | |
|         assertFileNames(refs, ['app.ts', 'dir.ts', 'dir2.ts']);
 | |
|       });
 | |
| 
 | |
|       it('finds rename locations for all matching directives', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): We could consider supporting rename for directive selectors in the future
 | |
|         // expect(renameLocations.length).toBe(3);
 | |
|         // assertTextSpans(renameLocations, ['dir']);
 | |
|         // assertFileNames(renameLocations, ['app.ts', 'dir.ts', 'dir2.ts']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on generic directive selector in template', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
| 
 | |
|         @Component({template: '<div *ngF¦or="let item of items"></div>'})
 | |
|         export class AppCmp {
 | |
|           items = [];
 | |
|         }
 | |
|       `);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
 | |
|         env = createModuleWithDeclarations([appFile]);
 | |
|       });
 | |
| 
 | |
|       it('should be able to request references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(refs.length).toBe(6);
 | |
|         assertTextSpans(refs, ['<div *ngFor="let item of items"></div>', 'NgForOf']);
 | |
|         assertFileNames(refs, ['index.d.ts', 'app.ts']);
 | |
|       });
 | |
| 
 | |
|       it('should not support rename if directive is in a dts file', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor);
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('components', () => {
 | |
|     describe('when cursor is on component class', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|       import {Component} from '@angular/core';
 | |
| 
 | |
|       @Component({selector: 'my-comp', template: ''})
 | |
|       export class MyCo¦mp {}`);
 | |
|         const appFile = `
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {MyComp} from './comp';
 | |
| 
 | |
|         @Component({template: '<my-comp></my-comp>'})
 | |
|         export class AppCmp {
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, MyComp]})
 | |
|         export class AppModule {}
 | |
|       `;
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: appFile, isRoot: true},
 | |
|           {name: _('/comp.ts'), contents: cursorInfo.text},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('finds references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/comp.ts'), cursor)!;
 | |
|         // 4 references are:  class declaration, template usage, app import and use in declarations
 | |
|         // list.
 | |
|         expect(refs.length).toBe(4);
 | |
|         assertTextSpans(refs, ['<my-comp>', 'MyComp']);
 | |
|         assertFileNames(refs, ['app.ts', 'comp.ts']);
 | |
|       });
 | |
| 
 | |
|       it('gets rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/comp.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): If we register as an exclusive provider for TS, we may need to return
 | |
|         // results here and should exclude the template results.
 | |
|         // expect(renameLocations.length).toBe(3);
 | |
|         // assertTextSpans(renameLocations, ['MyComp']);
 | |
|         // assertFileNames(renameLocations, ['app.ts', 'comp.ts']);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when cursor is on the element tag', () => {
 | |
|       let cursor: number;
 | |
|       beforeEach(() => {
 | |
|         const compFile = `
 | |
|       import {Component} from '@angular/core';
 | |
| 
 | |
|       @Component({selector: 'my-comp', template: ''})
 | |
|       export class MyComp {}`;
 | |
|         const cursorInfo = extractCursorInfo(`
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
|         import {MyComp} from './comp';
 | |
| 
 | |
|         @Component({template: '<my-c¦omp></my-comp>'})
 | |
|         export class AppCmp {
 | |
|         }
 | |
| 
 | |
|         @NgModule({declarations: [AppCmp, MyComp]})
 | |
|         export class AppModule {}
 | |
|       `);
 | |
|         cursor = cursorInfo.cursor;
 | |
|         env = LanguageServiceTestEnvironment.setup([
 | |
|           {name: _('/app.ts'), contents: cursorInfo.text, isRoot: true},
 | |
|           {name: _('/comp.ts'), contents: compFile},
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('gets references', () => {
 | |
|         const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
 | |
|         // 4 references are:  class declaration, template usage, app import and use in declarations
 | |
|         // list.
 | |
|         expect(refs.length).toBe(4);
 | |
|         assertTextSpans(refs, ['<my-comp>', 'MyComp']);
 | |
|         assertFileNames(refs, ['app.ts', 'comp.ts']);
 | |
|       });
 | |
| 
 | |
|       it('finds rename locations', () => {
 | |
|         const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
 | |
|         expect(renameLocations).toBeUndefined();
 | |
|         // TODO(atscott): We may consider supporting rename of component selector in the future
 | |
|         // expect(renameLocations.length).toBe(2);
 | |
|         // assertTextSpans(renameLocations, ['my-comp']);
 | |
|         // assertFileNames(renameLocations, ['app.ts', 'comp.ts']);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('get rename info', () => {
 | |
|     it('indicates inability to rename when cursor is outside template and in a string literal',
 | |
|        () => {
 | |
|          const {cursor, text} = extractCursorInfo(`
 | |
|             import {Component} from '@angular/core';
 | |
| 
 | |
|             @Component({selector: 'my-comp', template: ''})
 | |
|             export class MyComp {
 | |
|               myProp = 'cannot rena¦me me';
 | |
|             }`);
 | |
|          env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
 | |
|          env.expectNoSourceDiagnostics();
 | |
|          const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor);
 | |
|          expect(result.canRename).toEqual(false);
 | |
|        });
 | |
| 
 | |
|     it('gets rename info when cursor is outside template', () => {
 | |
|       const {cursor, text} = extractCursorInfo(`
 | |
|             import {Component, Input} from '@angular/core';
 | |
| 
 | |
|             @Component({name: 'my-comp', template: ''})
 | |
|             export class MyComp {
 | |
|               @Input() m¦yProp!: string;
 | |
|             }`);
 | |
|       env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
 | |
|       env.expectNoSourceDiagnostics();
 | |
|       const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
 | |
|       expect(result.canRename).toEqual(true);
 | |
|       expect(result.displayName).toEqual('myProp');
 | |
|       expect(result.kind).toEqual('property');
 | |
|     });
 | |
| 
 | |
|     it('gets rename info on keyed read', () => {
 | |
|       const {cursor, text} = extractCursorInfo(`
 | |
|             import {Component} from '@angular/core';
 | |
| 
 | |
|             @Component({name: 'my-comp', template: '{{ myObj["my¦Prop"] }}'})
 | |
|             export class MyComp {
 | |
|               readonly myObj = {'myProp': 'hello world'};
 | |
|             }`);
 | |
|       env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
 | |
|       env.expectNoSourceDiagnostics();
 | |
|       const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
 | |
|       expect(result.canRename).toEqual(true);
 | |
|       expect(result.displayName).toEqual('myProp');
 | |
|       expect(result.kind).toEqual('property');
 | |
|       expect(text.substring(
 | |
|                  result.triggerSpan.start, result.triggerSpan.start + result.triggerSpan.length))
 | |
|           .toBe('myProp');
 | |
|       // re-queries also work
 | |
|       const {triggerSpan, displayName} =
 | |
|           env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
 | |
|       expect(displayName).toEqual('myProp');
 | |
|       expect(text.substring(triggerSpan.start, triggerSpan.start + triggerSpan.length))
 | |
|           .toBe('myProp');
 | |
|     });
 | |
| 
 | |
|     it('gets rename info when cursor is on a directive input in a template', () => {
 | |
|       const dirFile = {
 | |
|         name: _('/dir.ts'),
 | |
|         contents: `
 | |
|         import {Directive, Input} from '@angular/core';
 | |
|         @Directive({selector: '[dir]'})
 | |
|         export class MyDir {
 | |
|           @Input() dir!: any;
 | |
|         }`
 | |
|       };
 | |
|       const {cursor, text} = extractCursorInfo(`
 | |
|             import {Component, Input} from '@angular/core';
 | |
| 
 | |
|             @Component({name: 'my-comp', template: '<div di¦r="something"></div>'})
 | |
|             export class MyComp {
 | |
|               @Input() myProp!: string;
 | |
|             }`);
 | |
|       env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}, dirFile]);
 | |
|       env.expectNoSourceDiagnostics();
 | |
|       const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
 | |
|       expect(result.canRename).toEqual(true);
 | |
|       expect(result.displayName).toEqual('dir');
 | |
|       expect(result.kind).toEqual('property');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   function getReferencesAtPosition(fileName: string, position: number) {
 | |
|     env.expectNoSourceDiagnostics();
 | |
|     const result = env.ngLS.getReferencesAtPosition(fileName, position);
 | |
|     return result?.map((item) => humanizeDocumentSpanLike(item, env));
 | |
|   }
 | |
| 
 | |
|   function getRenameLocationsAtPosition(fileName: string, position: number) {
 | |
|     env.expectNoSourceDiagnostics();
 | |
|     const result = env.ngLS.findRenameLocations(fileName, position);
 | |
|     return result?.map((item) => humanizeDocumentSpanLike(item, env));
 | |
|   }
 | |
| });
 |