Skip to content

Commit

Permalink
feat(language-service): add definitions for templateUrl
Browse files Browse the repository at this point in the history
Adds support for `getDefinitionAt` when called on a templateUrl
property assignment.

The currrent architecture for getting definitions is designed to be
called on templates, so we have to introduce a new
`getTsDefinitionAndBoundSpan` method to get Angular-specific definitions
in TypeScript files and pass a `readTemplate` closure that will read the
contents of a template using `TypeScriptServiceHost#getTemplates`. We
can probably go in and make this nicer in a future PR, though I'm not
sure what the best architecture should be yet.

Part of angular/vscode-ng-language-service#111
  • Loading branch information
ayazhafiz committed Aug 20, 2019
1 parent 639b732 commit 351317c
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 2 deletions.
61 changes: 60 additions & 1 deletion packages/language-service/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import * as path from 'path';
import * as ts from 'typescript'; // used as value and is provided at runtime
import {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Span} from './types';
import {Span, TemplateSource} from './types';
import {findTightestNode} from './utils';

/**
* Convert Angular Span to TypeScript TextSpan. Angular Span has 'start' and
Expand Down Expand Up @@ -53,3 +55,60 @@ export function getDefinitionAndBoundSpan(info: TemplateInfo): ts.DefinitionInfo
definitions, textSpan,
};
}

/**
* Gets an Angular-specific definition in a TypeScript source file.
*/
export function getTsDefinitionAndBoundSpan(
sf: ts.SourceFile, position: number,
readTemplate: (file: string) => TemplateSource[]): ts.DefinitionInfoAndBoundSpan|undefined {
const node = findTightestNode(sf, position);
if (!node) return;
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
// Attempt to extract definition of a URL in a property assignment.
return getUrlFromProperty(node as ts.StringLiteralLike, readTemplate);
default:
return undefined;
}
}

/**
* Attempts to get the definition of a file whose URL is specified in a property assignment.
* Currently applies to `templateUrl` properties.
*/
function getUrlFromProperty(
urlNode: ts.StringLiteralLike,
readTemplate: (file: string) => TemplateSource[]): ts.DefinitionInfoAndBoundSpan|undefined {
const sf = urlNode.getSourceFile();
const parent = urlNode.parent;
if (!ts.isPropertyAssignment(parent)) return;

switch (parent.name.getText()) {
case 'templateUrl':
// Extract definition of the template file specified by this `templateUrl` property.
const url = path.join(path.dirname(sf.fileName), urlNode.text);
const templates = readTemplate(url);
const templateDefinitions = templates.map(tmpl => {
return {
kind: ts.ScriptElementKind.externalModuleName,
name: url,
containerKind: ts.ScriptElementKind.unknown,
containerName: '',
textSpan: ngSpanToTsTextSpan(tmpl.span),
fileName: url,
};
});

return {
definitions: templateDefinitions,
textSpan: {
start: urlNode.getStart(),
length: urlNode.getWidth(),
},
};
default:
return undefined;
}
}
12 changes: 11 additions & 1 deletion packages/language-service/src/language_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import * as tss from 'typescript/lib/tsserverlibrary';
import {getTemplateCompletions, ngCompletionToTsCompletionEntry} from './completions';
import {getDefinitionAndBoundSpan} from './definitions';
import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics';
import {getHover} from './hover';
import {Diagnostic, LanguageService} from './types';
Expand Down Expand Up @@ -74,6 +74,16 @@ class LanguageServiceImpl implements LanguageService {
if (templateInfo) {
return getDefinitionAndBoundSpan(templateInfo);
}

// Attempt to get Angular-specific definitions in a TypeScript file, like templates defined
// in a `templateUrl` property.
if (fileName.endsWith('.ts')) {
const sf = this.host.getSourceFile(fileName);
if (sf) {
const readTemplate = this.host.getTemplates.bind(this.host);
return getTsDefinitionAndBoundSpan(sf, position, readTemplate);
}
}
}

getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined {
Expand Down
20 changes: 20 additions & 0 deletions packages/language-service/test/definitions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,26 @@ describe('definitions', () => {
}
});

it('should be able to find a template from a url', () => {
const fileName = addCode(`
@Component({
templateUrl: './«test».ng',
})
export class MyComponent {}`);

const marker = getReferenceMarkerFor(fileName, 'test');
const result = ngService.getDefinitionAt(fileName, marker.start);

expect(result).toBeDefined();
const {textSpan, definitions} = result !;

expect(definitions).toBeDefined();
expect(definitions !.length).toBe(1);
const [def] = definitions !;
expect(def.fileName).toBe('/app/test.ng');
expect(def.textSpan).toEqual({start: 0, length: 172});
});

/**
* Append a snippet of code to `app.component.ts` and return the file name.
* There must not be any name collision with existing code.
Expand Down

0 comments on commit 351317c

Please sign in to comment.