@import completion for css/scss/less. Fix #51331
This commit is contained in:
parent
37199daa9f
commit
a40bfc947c
|
@ -54,6 +54,26 @@
|
|||
],
|
||||
"smartStep": 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": {
|
||||
"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",
|
||||
"test": "mocha",
|
||||
"postinstall": "cd server && yarn install",
|
||||
"install-client-next": "yarn add vscode-languageclient@next"
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextE
|
|||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
import { ICompletionParticipant } from 'vscode-css-languageservice';
|
||||
|
||||
import { startsWith } from './utils/strings';
|
||||
import { startsWith, endsWith } from './utils/strings';
|
||||
|
||||
export function getPathCompletionParticipant(
|
||||
document: TextDocument,
|
||||
|
@ -21,32 +21,73 @@ export function getPathCompletionParticipant(
|
|||
): ICompletionParticipant {
|
||||
return {
|
||||
onCssURILiteralValue: ({ position, range, uriValue }) => {
|
||||
const isValueQuoted = startsWith(uriValue, `'`) || startsWith(uriValue, `"`);
|
||||
const fullValue = stripQuotes(uriValue);
|
||||
const valueBeforeCursor = isValueQuoted
|
||||
? fullValue.slice(0, position.character - (range.start.character + 1))
|
||||
: fullValue.slice(0, position.character - range.start.character);
|
||||
|
||||
if (fullValue === '.' || fullValue === '..') {
|
||||
result.isIncomplete = true;
|
||||
if (!shouldDoPathCompletion(uriValue, workspaceFolders)) {
|
||||
if (fullValue === '.' || fullValue === '..') {
|
||||
result.isIncomplete = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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));
|
||||
let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
|
||||
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('|');
|
||||
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);
|
||||
|
||||
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 folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||
|
||||
|
@ -121,7 +121,7 @@ suite('Completions', () => {
|
|||
}, 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 folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }];
|
||||
|
||||
|
@ -149,4 +149,50 @@ suite('Completions', () => {
|
|||
]
|
||||
}, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
Loading…
Reference in a new issue