Skip to content

Commit a391aeb

Browse files
ayazhafizmatsko
authored andcommitted
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
1 parent c3a1ef2 commit a391aeb

File tree

7 files changed

+144
-40
lines changed

7 files changed

+144
-40
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"seq": 0,
3+
"type": "response",
4+
"command": "definitionAndBoundSpan",
5+
"request_seq": 3,
6+
"success": true,
7+
"body": {
8+
"definitions": [
9+
{
10+
"file": "${PWD}/project/app/style.css",
11+
"start": {
12+
"line": 1,
13+
"offset": 1
14+
},
15+
"end": {
16+
"line": 1,
17+
"offset": 1
18+
}
19+
}
20+
],
21+
"textSpan": {
22+
"start": {
23+
"line": 6,
24+
"offset": 16
25+
},
26+
"end": {
27+
"line": 6,
28+
"offset": 27
29+
}
30+
}
31+
}
32+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
body,
2+
html {
3+
width: 100%;
4+
}

integration/language_service_plugin/project/app/widget.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { Component } from '@angular/core';
33
@Component({
44
selector: 'my-widget',
55
templateUrl: './widget.component.html',
6+
styleUrls: ['./style.css'],
67
})
78
export class WidgetComponent { name = 'Angular'; }

integration/language_service_plugin/test.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {Client} from './tsclient';
55

66
describe('Angular Language Service', () => {
77
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */
8-
const PWD = process.env.PWD !;
8+
const PWD = process.env.PWD!;
99
const SERVER_PATH = './node_modules/typescript/lib/tsserver.js';
1010
let server: ChildProcess;
1111
let client: Client;
@@ -27,14 +27,14 @@ describe('Angular Language Service', () => {
2727
client.listen();
2828
});
2929

30-
afterEach(async() => {
30+
afterEach(async () => {
3131
client.sendRequest('exit', {});
3232

3333
// Give server process some time to flush all messages
3434
await new Promise((resolve) => setTimeout(resolve, 1000));
3535
});
3636

37-
it('should be launched as tsserver plugin', async() => {
37+
it('should be launched as tsserver plugin', async () => {
3838
let response = await client.sendRequest('configure', {
3939
hostInfo: 'vscode',
4040
});
@@ -60,7 +60,7 @@ describe('Angular Language Service', () => {
6060
client.sendRequest('geterr', {delay: 0, files: [`${PWD}/project/app/app.module.ts`]});
6161
});
6262

63-
it('should perform completions', async() => {
63+
it('should perform completions', async () => {
6464
await client.sendRequest('configure', {
6565
hostInfo: 'vscode',
6666
});
@@ -98,7 +98,7 @@ describe('Angular Language Service', () => {
9898
expect(response).toMatchGolden('completionInfo.json');
9999
});
100100

101-
it('should perform quickinfo', async() => {
101+
it('should perform quickinfo', async () => {
102102
client.sendRequest('open', {
103103
file: `${PWD}/project/app/app.component.ts`,
104104
});
@@ -118,7 +118,7 @@ describe('Angular Language Service', () => {
118118
expect(resp2).toMatchGolden('quickinfo.json');
119119
});
120120

121-
it('should perform definition', async() => {
121+
it('should perform definition', async () => {
122122
client.sendRequest('open', {
123123
file: `${PWD}/project/app/app.component.ts`,
124124
});
@@ -138,7 +138,7 @@ describe('Angular Language Service', () => {
138138
expect(resp2).toMatchGolden('definition.json');
139139
});
140140

141-
it('should perform definitionAndBoundSpan', async() => {
141+
it('should perform definitionAndBoundSpan', async () => {
142142
client.sendRequest('open', {
143143
file: `${PWD}/project/app/app.component.ts`,
144144
});
@@ -158,7 +158,7 @@ describe('Angular Language Service', () => {
158158
expect(resp2).toMatchGolden('definitionAndBoundSpan.json');
159159
});
160160

161-
it('should perform definitionAndBoundSpan for template URLs', async() => {
161+
it('should perform definitionAndBoundSpan for template URLs', async () => {
162162
client.sendRequest('open', {
163163
file: `${PWD}/project/app/widget.component.ts`,
164164
});
@@ -177,4 +177,27 @@ describe('Angular Language Service', () => {
177177
});
178178
expect(resp2).toMatchGolden('templateUrlDefinition.json');
179179
});
180+
181+
it('should perform definitionAndBoundSpan for style URLs', async () => {
182+
client.sendRequest('open', {
183+
file: `${PWD}/project/app/widget.component.ts`,
184+
});
185+
client.sendRequest('open', {
186+
file: `${PWD}/project/app/style.css`,
187+
});
188+
189+
const resp1 = await client.sendRequest('reload', {
190+
file: `${PWD}/project/app/widget.component.ts`,
191+
tmpFile: `${PWD}/project/app/widget.component.ts`,
192+
}) as any;
193+
expect(resp1.command).toBe('reload');
194+
expect(resp1.success).toBe(true);
195+
196+
const resp2 = await client.sendRequest('definitionAndBoundSpan', {
197+
file: `${PWD}/project/app/widget.component.ts`,
198+
line: 6,
199+
offset: 18,
200+
});
201+
expect(resp2).toMatchGolden('styleUrlsDefinition.json');
202+
});
180203
});

packages/language-service/src/definitions.ts

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -84,45 +84,61 @@ export function getTsDefinitionAndBoundSpan(
8484
/**
8585
* Attempts to get the definition of a file whose URL is specified in a property assignment in a
8686
* directive decorator.
87-
* Currently applies to `templateUrl` properties.
87+
* Currently applies to `templateUrl` and `styleUrls` properties.
8888
*/
8989
function getUrlFromProperty(
9090
urlNode: ts.StringLiteralLike,
9191
tsLsHost: Readonly<ts.LanguageServiceHost>): ts.DefinitionInfoAndBoundSpan|undefined {
92-
const asgn = getPropertyAssignmentFromValue(urlNode);
93-
if (!asgn) return;
94-
// If the URL is not a property of a class decorator, don't generate definitions for it.
92+
// Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment.
93+
// These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is
94+
// an array of strings:
95+
// {
96+
// templateUrl: './template.ng.html',
97+
// styleUrls: ['./style.css', './other-style.css']
98+
// }
99+
// `templateUrl`'s property assignment can be found from the string literal node;
100+
// `styleUrls`'s property assignment can be found from the array (parent) node.
101+
//
102+
// First search for `templateUrl`.
103+
let asgn = getPropertyAssignmentFromValue(urlNode);
104+
if (!asgn || asgn.name.getText() !== 'templateUrl') {
105+
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
106+
asgn = getPropertyAssignmentFromValue(urlNode.parent);
107+
if (!asgn || asgn.name.getText() !== 'styleUrls') {
108+
// Nothing found, bail.
109+
return;
110+
}
111+
}
112+
113+
// If the property assignment is not a property of a class decorator, don't generate definitions
114+
// for it.
95115
if (!isClassDecoratorProperty(asgn)) return;
96116

97117
const sf = urlNode.getSourceFile();
98-
switch (asgn.name.getText()) {
99-
case 'templateUrl':
100-
// Extract definition of the template file specified by this `templateUrl` property.
101-
const url = path.join(path.dirname(sf.fileName), urlNode.text);
118+
// Extract url path specified by the url node, which is relative to the TypeScript source file
119+
// the url node is defined in.
120+
const url = path.join(path.dirname(sf.fileName), urlNode.text);
102121

103-
// If the file does not exist, bail. It is possible that the TypeScript language service host
104-
// does not have a `fileExists` method, in which case optimistically assume the file exists.
105-
if (tsLsHost.fileExists && !tsLsHost.fileExists(url)) return;
122+
// If the file does not exist, bail. It is possible that the TypeScript language service host
123+
// does not have a `fileExists` method, in which case optimistically assume the file exists.
124+
if (tsLsHost.fileExists && !tsLsHost.fileExists(url)) return;
106125

107-
const templateDefinitions: ts.DefinitionInfo[] = [{
108-
kind: ts.ScriptElementKind.externalModuleName,
109-
name: url,
110-
containerKind: ts.ScriptElementKind.unknown,
111-
containerName: '',
112-
// Reading the template is expensive, so don't provide a preview.
113-
textSpan: {start: 0, length: 0},
114-
fileName: url,
115-
}];
126+
const templateDefinitions: ts.DefinitionInfo[] = [{
127+
kind: ts.ScriptElementKind.externalModuleName,
128+
name: url,
129+
containerKind: ts.ScriptElementKind.unknown,
130+
containerName: '',
131+
// Reading the template is expensive, so don't provide a preview.
132+
textSpan: {start: 0, length: 0},
133+
fileName: url,
134+
}];
116135

117-
return {
118-
definitions: templateDefinitions,
119-
textSpan: {
120-
// Exclude opening and closing quotes in the url span.
121-
start: urlNode.getStart() + 1,
122-
length: urlNode.getWidth() - 2,
123-
},
124-
};
125-
default:
126-
return undefined;
127-
}
136+
return {
137+
definitions: templateDefinitions,
138+
textSpan: {
139+
// Exclude opening and closing quotes in the url span.
140+
start: urlNode.getStart() + 1,
141+
length: urlNode.getWidth() - 2,
142+
},
143+
};
128144
}

packages/language-service/test/definitions_spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,27 @@ describe('definitions', () => {
275275
expect(def.fileName).toBe('/app/test.ng');
276276
expect(def.textSpan).toEqual({start: 0, length: 0});
277277
});
278+
279+
it('should be able to find a stylesheet from a url', () => {
280+
const fileName = mockHost.addCode(`
281+
@Component({
282+
templateUrl: './test.ng',
283+
styleUrls: ['./«test».css'],
284+
})
285+
export class MyComponent {}`);
286+
287+
const marker = mockHost.getReferenceMarkerFor(fileName, 'test');
288+
const result = ngService.getDefinitionAt(fileName, marker.start);
289+
290+
expect(result).toBeDefined();
291+
const {textSpan, definitions} = result !;
292+
293+
expect(textSpan).toEqual({start: marker.start - 2, length: 10});
294+
295+
expect(definitions).toBeDefined();
296+
expect(definitions !.length).toBe(1);
297+
const [def] = definitions !;
298+
expect(def.fileName).toBe('/app/test.css');
299+
expect(def.textSpan).toEqual({start: 0, length: 0});
300+
});
278301
});

packages/language-service/test/test_data.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ export class ShowIf {
232232
<label>name: </label>
233233
</div>
234234
&~{entity-amp}amp;
235-
`
235+
`,
236+
'test.css': `
237+
body, html {
238+
width: 100%;
239+
}
240+
`,
236241
}
237242
};

0 commit comments

Comments
 (0)