Skip to content

Commit

Permalink
@import completion for css/scss/less. Fix #51331
Browse files Browse the repository at this point in the history
  • Loading branch information
octref committed Aug 7, 2018
1 parent 37199da commit a40bfc9
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 19 deletions.
20 changes: 20 additions & 0 deletions extensions/css-language-features/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"
]
}
]
}
1 change: 1 addition & 0 deletions extensions/css-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
71 changes: 56 additions & 15 deletions extensions/css-language-features/server/src/pathCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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() }];

Expand Down Expand Up @@ -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() }];

Expand Down Expand Up @@ -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');
});
});
14 changes: 14 additions & 0 deletions extensions/css-language-features/server/src/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*--------------------------------------------------------------------------------------------*/
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions extensions/css-language-features/test/mocha.opts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--ui tdd
--useColors true
server/out/test/**.test.js

0 comments on commit a40bfc9

Please sign in to comment.