feat(language-service): add definitions for styleUrls (#32464)

Adds support for `styleUrls` definitions in the same way `templateUrl`
definitions are provided; clicking on styleUrl will take a user to the
respective file.

Unifies some code in determining a URL definition. We first check if a
url is a `templateUrl`; if it's not, we check that it's a `styleUrl` or
return no definitions.

PR Close #32464
This commit is contained in:
ayazhafiz 2019-09-01 09:56:29 -05:00 committed by Matias Niemelä
parent c3a1ef219e
commit a391aebbcf
7 changed files with 144 additions and 40 deletions

View File

@ -0,0 +1,32 @@
{
"seq": 0,
"type": "response",
"command": "definitionAndBoundSpan",
"request_seq": 3,
"success": true,
"body": {
"definitions": [
{
"file": "${PWD}/project/app/style.css",
"start": {
"line": 1,
"offset": 1
},
"end": {
"line": 1,
"offset": 1
}
}
],
"textSpan": {
"start": {
"line": 6,
"offset": 16
},
"end": {
"line": 6,
"offset": 27
}
}
}
}

View File

@ -0,0 +1,4 @@
body,
html {
width: 100%;
}

View File

@ -3,5 +3,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'my-widget', selector: 'my-widget',
templateUrl: './widget.component.html', templateUrl: './widget.component.html',
styleUrls: ['./style.css'],
}) })
export class WidgetComponent { name = 'Angular'; } export class WidgetComponent { name = 'Angular'; }

View File

@ -177,4 +177,27 @@ describe('Angular Language Service', () => {
}); });
expect(resp2).toMatchGolden('templateUrlDefinition.json'); expect(resp2).toMatchGolden('templateUrlDefinition.json');
}); });
it('should perform definitionAndBoundSpan for style URLs', async () => {
client.sendRequest('open', {
file: `${PWD}/project/app/widget.component.ts`,
});
client.sendRequest('open', {
file: `${PWD}/project/app/style.css`,
});
const resp1 = await client.sendRequest('reload', {
file: `${PWD}/project/app/widget.component.ts`,
tmpFile: `${PWD}/project/app/widget.component.ts`,
}) as any;
expect(resp1.command).toBe('reload');
expect(resp1.success).toBe(true);
const resp2 = await client.sendRequest('definitionAndBoundSpan', {
file: `${PWD}/project/app/widget.component.ts`,
line: 6,
offset: 18,
});
expect(resp2).toMatchGolden('styleUrlsDefinition.json');
});
}); });

View File

@ -84,20 +84,39 @@ export function getTsDefinitionAndBoundSpan(
/** /**
* Attempts to get the definition of a file whose URL is specified in a property assignment in a * Attempts to get the definition of a file whose URL is specified in a property assignment in a
* directive decorator. * directive decorator.
* Currently applies to `templateUrl` properties. * Currently applies to `templateUrl` and `styleUrls` properties.
*/ */
function getUrlFromProperty( function getUrlFromProperty(
urlNode: ts.StringLiteralLike, urlNode: ts.StringLiteralLike,
tsLsHost: Readonly<ts.LanguageServiceHost>): ts.DefinitionInfoAndBoundSpan|undefined { tsLsHost: Readonly<ts.LanguageServiceHost>): ts.DefinitionInfoAndBoundSpan|undefined {
const asgn = getPropertyAssignmentFromValue(urlNode); // Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment.
if (!asgn) return; // These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is
// If the URL is not a property of a class decorator, don't generate definitions for it. // an array of strings:
// {
// templateUrl: './template.ng.html',
// styleUrls: ['./style.css', './other-style.css']
// }
// `templateUrl`'s property assignment can be found from the string literal node;
// `styleUrls`'s property assignment can be found from the array (parent) node.
//
// First search for `templateUrl`.
let asgn = getPropertyAssignmentFromValue(urlNode);
if (!asgn || asgn.name.getText() !== 'templateUrl') {
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
asgn = getPropertyAssignmentFromValue(urlNode.parent);
if (!asgn || asgn.name.getText() !== 'styleUrls') {
// Nothing found, bail.
return;
}
}
// If the property assignment is not a property of a class decorator, don't generate definitions
// for it.
if (!isClassDecoratorProperty(asgn)) return; if (!isClassDecoratorProperty(asgn)) return;
const sf = urlNode.getSourceFile(); const sf = urlNode.getSourceFile();
switch (asgn.name.getText()) { // Extract url path specified by the url node, which is relative to the TypeScript source file
case 'templateUrl': // the url node is defined in.
// Extract definition of the template file specified by this `templateUrl` property.
const url = path.join(path.dirname(sf.fileName), urlNode.text); const url = path.join(path.dirname(sf.fileName), urlNode.text);
// If the file does not exist, bail. It is possible that the TypeScript language service host // If the file does not exist, bail. It is possible that the TypeScript language service host
@ -122,7 +141,4 @@ function getUrlFromProperty(
length: urlNode.getWidth() - 2, length: urlNode.getWidth() - 2,
}, },
}; };
default:
return undefined;
}
} }

View File

@ -275,4 +275,27 @@ describe('definitions', () => {
expect(def.fileName).toBe('/app/test.ng'); expect(def.fileName).toBe('/app/test.ng');
expect(def.textSpan).toEqual({start: 0, length: 0}); expect(def.textSpan).toEqual({start: 0, length: 0});
}); });
it('should be able to find a stylesheet from a url', () => {
const fileName = mockHost.addCode(`
@Component({
templateUrl: './test.ng',
styleUrls: ['./«test».css'],
})
export class MyComponent {}`);
const marker = mockHost.getReferenceMarkerFor(fileName, 'test');
const result = ngService.getDefinitionAt(fileName, marker.start);
expect(result).toBeDefined();
const {textSpan, definitions} = result !;
expect(textSpan).toEqual({start: marker.start - 2, length: 10});
expect(definitions).toBeDefined();
expect(definitions !.length).toBe(1);
const [def] = definitions !;
expect(def.fileName).toBe('/app/test.css');
expect(def.textSpan).toEqual({start: 0, length: 0});
});
}); });

View File

@ -232,6 +232,11 @@ export class ShowIf {
<label>name: </label> <label>name: </label>
</div> </div>
&~{entity-amp}amp; &~{entity-amp}amp;
` `,
'test.css': `
body, html {
width: 100%;
}
`,
} }
}; };