mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 04:09:28 +00:00
@import completion for css/scss/less. Fix #51331
This commit is contained in:
@@ -54,6 +54,26 @@
|
|||||||
],
|
],
|
||||||
"smartStep": true,
|
"smartStep": true,
|
||||||
"restart": true
|
"restart": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Server Unit Tests",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||||
|
"stopOnEntry": false,
|
||||||
|
"args": [
|
||||||
|
"--timeout",
|
||||||
|
"999999",
|
||||||
|
"--colors"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"runtimeExecutable": null,
|
||||||
|
"runtimeArgs": [],
|
||||||
|
"env": {},
|
||||||
|
"sourceMaps": true,
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceRoot}/server/out/**"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server",
|
"compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server",
|
||||||
"watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server",
|
"watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server",
|
||||||
|
"test": "mocha",
|
||||||
"postinstall": "cd server && yarn install",
|
"postinstall": "cd server && yarn install",
|
||||||
"install-client-next": "yarn add vscode-languageclient@next"
|
"install-client-next": "yarn add vscode-languageclient@next"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextE
|
|||||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||||
import { ICompletionParticipant } from 'vscode-css-languageservice';
|
import { ICompletionParticipant } from 'vscode-css-languageservice';
|
||||||
|
|
||||||
import { startsWith } from './utils/strings';
|
import { startsWith, endsWith } from './utils/strings';
|
||||||
|
|
||||||
export function getPathCompletionParticipant(
|
export function getPathCompletionParticipant(
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
@@ -21,32 +21,73 @@ export function getPathCompletionParticipant(
|
|||||||
): ICompletionParticipant {
|
): ICompletionParticipant {
|
||||||
return {
|
return {
|
||||||
onCssURILiteralValue: ({ position, range, uriValue }) => {
|
onCssURILiteralValue: ({ position, range, uriValue }) => {
|
||||||
const isValueQuoted = startsWith(uriValue, `'`) || startsWith(uriValue, `"`);
|
|
||||||
const fullValue = stripQuotes(uriValue);
|
const fullValue = stripQuotes(uriValue);
|
||||||
const valueBeforeCursor = isValueQuoted
|
if (!shouldDoPathCompletion(uriValue, workspaceFolders)) {
|
||||||
? fullValue.slice(0, position.character - (range.start.character + 1))
|
if (fullValue === '.' || fullValue === '..') {
|
||||||
: fullValue.slice(0, position.character - range.start.character);
|
result.isIncomplete = true;
|
||||||
|
}
|
||||||
if (fullValue === '.' || fullValue === '..') {
|
|
||||||
result.isIncomplete = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
let suggestions = providePathSuggestions(uriValue, position, range, document, workspaceFolders);
|
||||||
|
result.items = [...suggestions, ...result.items];
|
||||||
|
},
|
||||||
|
onCssImportPath: ({ position, range, pathValue }) => {
|
||||||
|
const fullValue = stripQuotes(pathValue);
|
||||||
|
if (!shouldDoPathCompletion(pathValue, workspaceFolders)) {
|
||||||
|
if (fullValue === '.' || fullValue === '..') {
|
||||||
|
result.isIncomplete = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
|
|
||||||
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
|
|
||||||
|
|
||||||
const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
|
let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders);
|
||||||
const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);
|
|
||||||
const suggestions = paths.map(p => pathToSuggestion(p, replaceRange));
|
if (document.languageId === 'scss') {
|
||||||
|
suggestions.forEach(s => {
|
||||||
|
if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) {
|
||||||
|
if (s.textEdit) {
|
||||||
|
s.textEdit.newText = s.label.slice(1, -5);
|
||||||
|
} else {
|
||||||
|
s.label = s.label.slice(1, -5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
result.items = [...suggestions, ...result.items];
|
result.items = [...suggestions, ...result.items];
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, workspaceFolders: WorkspaceFolder[]) {
|
||||||
|
const fullValue = stripQuotes(pathValue);
|
||||||
|
const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`);
|
||||||
|
const valueBeforeCursor = isValueQuoted
|
||||||
|
? fullValue.slice(0, position.character - (range.start.character + 1))
|
||||||
|
: fullValue.slice(0, position.character - range.start.character);
|
||||||
|
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
|
||||||
|
|
||||||
|
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
|
||||||
|
const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
|
||||||
|
const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);
|
||||||
|
|
||||||
|
const suggestions = paths.map(p => pathToSuggestion(p, replaceRange));
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldDoPathCompletion(pathValue: string, workspaceFolders: WorkspaceFolder[]): boolean {
|
||||||
|
const fullValue = stripQuotes(pathValue);
|
||||||
|
if (fullValue === '.' || fullValue === '..') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function stripQuotes(fullValue: string) {
|
function stripQuotes(fullValue: string) {
|
||||||
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
|
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
|
||||||
return fullValue.slice(1, -1);
|
return fullValue.slice(1, -1);
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ suite('Completions', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[]): void {
|
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[], lang: string = 'css'): void {
|
||||||
const offset = value.indexOf('|');
|
const offset = value.indexOf('|');
|
||||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||||
|
|
||||||
const document = TextDocument.create(testUri, 'css', 0, value);
|
const document = TextDocument.create(testUri, lang, 0, value);
|
||||||
const position = document.positionAt(offset);
|
const position = document.positionAt(offset);
|
||||||
|
|
||||||
if (!workspaceFolders) {
|
if (!workspaceFolders) {
|
||||||
@@ -61,7 +61,7 @@ suite('Completions', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('CSS Path completion', function () {
|
test('CSS url() Path completion', function () {
|
||||||
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
||||||
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ suite('Completions', () => {
|
|||||||
}, testUri, folders);
|
}, testUri, folders);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CSS Path Completion - Unquoted url', function () {
|
test('CSS url() Path Completion - Unquoted url', function () {
|
||||||
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
||||||
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||||
|
|
||||||
@@ -149,4 +149,50 @@ suite('Completions', () => {
|
|||||||
]
|
]
|
||||||
}, testUri, folders);
|
}, testUri, folders);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('CSS @import Path completion', function () {
|
||||||
|
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
||||||
|
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||||
|
|
||||||
|
assertCompletions(`@import './|'`, {
|
||||||
|
items: [
|
||||||
|
{ label: 'about.css', resultText: `@import './about.css'` },
|
||||||
|
{ label: 'about.html', resultText: `@import './about.html'` },
|
||||||
|
]
|
||||||
|
}, testUri, folders);
|
||||||
|
|
||||||
|
assertCompletions(`@import '../|'`, {
|
||||||
|
items: [
|
||||||
|
{ label: 'about/', resultText: `@import '../about/'` },
|
||||||
|
{ label: 'scss/', resultText: `@import '../scss/'` },
|
||||||
|
{ label: 'index.html', resultText: `@import '../index.html'` },
|
||||||
|
{ label: 'src/', resultText: `@import '../src/'` }
|
||||||
|
]
|
||||||
|
}, testUri, folders);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For SCSS, `@import 'foo';` can be used for importing partial file `_foo.scss`
|
||||||
|
*/
|
||||||
|
test('SCSS @import Path completion', function () {
|
||||||
|
let testCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString();
|
||||||
|
let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are in a CSS file, so no special treatment for SCSS partial files
|
||||||
|
*/
|
||||||
|
assertCompletions(`@import '../scss/|'`, {
|
||||||
|
items: [
|
||||||
|
{ label: 'main.scss', resultText: `@import '../scss/main.scss'` },
|
||||||
|
{ label: '_foo.scss', resultText: `@import '../scss/_foo.scss'` }
|
||||||
|
]
|
||||||
|
}, testCSSUri, folders);
|
||||||
|
|
||||||
|
let testSCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/scss/main.scss')).toString();
|
||||||
|
assertCompletions(`@import './|'`, {
|
||||||
|
items: [
|
||||||
|
{ label: '_foo.scss', resultText: `@import './foo'` }
|
||||||
|
]
|
||||||
|
}, testSCSSUri, folders, 'scss');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -17,3 +17,17 @@ export function startsWith(haystack: string, needle: string): boolean {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if haystack ends with needle.
|
||||||
|
*/
|
||||||
|
export function endsWith(haystack: string, needle: string): boolean {
|
||||||
|
let diff = haystack.length - needle.length;
|
||||||
|
if (diff > 0) {
|
||||||
|
return haystack.lastIndexOf(needle) === diff;
|
||||||
|
} else if (diff === 0) {
|
||||||
|
return haystack === needle;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
3
extensions/css-language-features/test/mocha.opts
Normal file
3
extensions/css-language-features/test/mocha.opts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
--ui tdd
|
||||||
|
--useColors true
|
||||||
|
server/out/test/**.test.js
|
||||||
Reference in New Issue
Block a user