Skip to content

Commit 803ca90

Browse files
authored
Merge pull request #55971 from Microsoft/octref/css-import-completion
@import completion for css/scss/less. Fix #51331
2 parents e9b3304 + a40bfc9 commit 803ca90

File tree

8 files changed

+152
-19
lines changed

8 files changed

+152
-19
lines changed

extensions/css-language-features/.vscode/launch.json

+20
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@
5454
],
5555
"smartStep": true,
5656
"restart": true
57+
},
58+
{
59+
"name": "Server Unit Tests",
60+
"type": "node",
61+
"request": "launch",
62+
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
63+
"stopOnEntry": false,
64+
"args": [
65+
"--timeout",
66+
"999999",
67+
"--colors"
68+
],
69+
"cwd": "${workspaceRoot}",
70+
"runtimeExecutable": null,
71+
"runtimeArgs": [],
72+
"env": {},
73+
"sourceMaps": true,
74+
"outFiles": [
75+
"${workspaceRoot}/server/out/**"
76+
]
5777
}
5878
]
5979
}

extensions/css-language-features/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"scripts": {
2020
"compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server",
2121
"watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server",
22+
"test": "mocha",
2223
"postinstall": "cd server && yarn install",
2324
"install-client-next": "yarn add vscode-languageclient@next"
2425
},

extensions/css-language-features/server/src/pathCompletion.ts

+56-15
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextE
1212
import { WorkspaceFolder } from 'vscode-languageserver';
1313
import { ICompletionParticipant } from 'vscode-css-languageservice';
1414

15-
import { startsWith } from './utils/strings';
15+
import { startsWith, endsWith } from './utils/strings';
1616

1717
export function getPathCompletionParticipant(
1818
document: TextDocument,
@@ -21,32 +21,73 @@ export function getPathCompletionParticipant(
2121
): ICompletionParticipant {
2222
return {
2323
onCssURILiteralValue: ({ position, range, uriValue }) => {
24-
const isValueQuoted = startsWith(uriValue, `'`) || startsWith(uriValue, `"`);
2524
const fullValue = stripQuotes(uriValue);
26-
const valueBeforeCursor = isValueQuoted
27-
? fullValue.slice(0, position.character - (range.start.character + 1))
28-
: fullValue.slice(0, position.character - range.start.character);
29-
30-
if (fullValue === '.' || fullValue === '..') {
31-
result.isIncomplete = true;
25+
if (!shouldDoPathCompletion(uriValue, workspaceFolders)) {
26+
if (fullValue === '.' || fullValue === '..') {
27+
result.isIncomplete = true;
28+
}
3229
return;
3330
}
3431

35-
if (!workspaceFolders || workspaceFolders.length === 0) {
32+
let suggestions = providePathSuggestions(uriValue, position, range, document, workspaceFolders);
33+
result.items = [...suggestions, ...result.items];
34+
},
35+
onCssImportPath: ({ position, range, pathValue }) => {
36+
const fullValue = stripQuotes(pathValue);
37+
if (!shouldDoPathCompletion(pathValue, workspaceFolders)) {
38+
if (fullValue === '.' || fullValue === '..') {
39+
result.isIncomplete = true;
40+
}
3641
return;
3742
}
38-
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
39-
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
4043

41-
const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
42-
const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);
43-
const suggestions = paths.map(p => pathToSuggestion(p, replaceRange));
44+
let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders);
45+
46+
if (document.languageId === 'scss') {
47+
suggestions.forEach(s => {
48+
if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) {
49+
if (s.textEdit) {
50+
s.textEdit.newText = s.label.slice(1, -5);
51+
} else {
52+
s.label = s.label.slice(1, -5);
53+
}
54+
}
55+
});
56+
}
4457
result.items = [...suggestions, ...result.items];
4558
}
46-
4759
};
4860
}
4961

62+
function providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, workspaceFolders: WorkspaceFolder[]) {
63+
const fullValue = stripQuotes(pathValue);
64+
const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`);
65+
const valueBeforeCursor = isValueQuoted
66+
? fullValue.slice(0, position.character - (range.start.character + 1))
67+
: fullValue.slice(0, position.character - range.start.character);
68+
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
69+
70+
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
71+
const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
72+
const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);
73+
74+
const suggestions = paths.map(p => pathToSuggestion(p, replaceRange));
75+
return suggestions;
76+
}
77+
78+
function shouldDoPathCompletion(pathValue: string, workspaceFolders: WorkspaceFolder[]): boolean {
79+
const fullValue = stripQuotes(pathValue);
80+
if (fullValue === '.' || fullValue === '..') {
81+
return false;
82+
}
83+
84+
if (!workspaceFolders || workspaceFolders.length === 0) {
85+
return false;
86+
}
87+
88+
return true;
89+
}
90+
5091
function stripQuotes(fullValue: string) {
5192
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
5293
return fullValue.slice(1, -1);

extensions/css-language-features/server/src/test/completion.test.ts

+50-4
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ suite('Completions', () => {
3333
}
3434
};
3535

36-
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[]): void {
36+
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[], lang: string = 'css'): void {
3737
const offset = value.indexOf('|');
3838
value = value.substr(0, offset) + value.substr(offset + 1);
3939

40-
const document = TextDocument.create(testUri, 'css', 0, value);
40+
const document = TextDocument.create(testUri, lang, 0, value);
4141
const position = document.positionAt(offset);
4242

4343
if (!workspaceFolders) {
@@ -61,7 +61,7 @@ suite('Completions', () => {
6161
}
6262
}
6363

64-
test('CSS Path completion', function () {
64+
test('CSS url() Path completion', function () {
6565
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
6666
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
6767

@@ -121,7 +121,7 @@ suite('Completions', () => {
121121
}, testUri, folders);
122122
});
123123

124-
test('CSS Path Completion - Unquoted url', function () {
124+
test('CSS url() Path Completion - Unquoted url', function () {
125125
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
126126
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
127127

@@ -149,4 +149,50 @@ suite('Completions', () => {
149149
]
150150
}, testUri, folders);
151151
});
152+
153+
test('CSS @import Path completion', function () {
154+
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
155+
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
156+
157+
assertCompletions(`@import './|'`, {
158+
items: [
159+
{ label: 'about.css', resultText: `@import './about.css'` },
160+
{ label: 'about.html', resultText: `@import './about.html'` },
161+
]
162+
}, testUri, folders);
163+
164+
assertCompletions(`@import '../|'`, {
165+
items: [
166+
{ label: 'about/', resultText: `@import '../about/'` },
167+
{ label: 'scss/', resultText: `@import '../scss/'` },
168+
{ label: 'index.html', resultText: `@import '../index.html'` },
169+
{ label: 'src/', resultText: `@import '../src/'` }
170+
]
171+
}, testUri, folders);
172+
});
173+
174+
/**
175+
* For SCSS, `@import 'foo';` can be used for importing partial file `_foo.scss`
176+
*/
177+
test('SCSS @import Path completion', function () {
178+
let testCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
179+
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
180+
181+
/**
182+
* We are in a CSS file, so no special treatment for SCSS partial files
183+
*/
184+
assertCompletions(`@import '../scss/|'`, {
185+
items: [
186+
{ label: 'main.scss', resultText: `@import '../scss/main.scss'` },
187+
{ label: '_foo.scss', resultText: `@import '../scss/_foo.scss'` }
188+
]
189+
}, testCSSUri, folders);
190+
191+
let testSCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/scss/main.scss')).toString();
192+
assertCompletions(`@import './|'`, {
193+
items: [
194+
{ label: '_foo.scss', resultText: `@import './foo'` }
195+
]
196+
}, testSCSSUri, folders, 'scss');
197+
});
152198
});

extensions/css-language-features/server/src/utils/strings.ts

+14
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,17 @@ export function startsWith(haystack: string, needle: string): boolean {
1717

1818
return true;
1919
}
20+
21+
/**
22+
* Determines if haystack ends with needle.
23+
*/
24+
export function endsWith(haystack: string, needle: string): boolean {
25+
let diff = haystack.length - needle.length;
26+
if (diff > 0) {
27+
return haystack.lastIndexOf(needle) === diff;
28+
} else if (diff === 0) {
29+
return haystack === needle;
30+
} else {
31+
return false;
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--ui tdd
2+
--useColors true
3+
server/out/test/**.test.js

0 commit comments

Comments
 (0)