未验证 提交 803ca903 编写于 作者: M Martin Aeschlimann 提交者: GitHub

Merge pull request #55971 from Microsoft/octref/css-import-completion

@import completion for css/scss/less. Fix #51331
......@@ -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/**"
]
}
]
}
\ No newline at end of file
......@@ -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');
});
});
\ No newline at end of file
......@@ -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;
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
\ No newline at end of file
--ui tdd
--useColors true
server/out/test/**.test.js
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册