Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

path completion for css. fix #45235 #45788

Merged
merged 4 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion extensions/css/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"version": "0.2.0",
"compounds": [
{
"name": "Debug Extension and Language Server",
"configurations": ["Launch Extension", "Attach Language Server"]
}
],
"configurations": [
{
"name": "Launch Extension",
Expand Down Expand Up @@ -32,7 +38,8 @@
"protocol": "inspector",
"port": 6044,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/server/out/**/*.js"]
"outFiles": ["${workspaceFolder}/server/out/**/*.js"],
"restart": true
}
]
}
19 changes: 15 additions & 4 deletions extensions/css/server/src/cssServerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
ConfigurationRequest, WorkspaceFolder, DocumentColorRequest, ColorPresentationRequest
} from 'vscode-languageserver';

import { TextDocument } from 'vscode-languageserver-types';
import { TextDocument, CompletionList } from 'vscode-languageserver-types';

import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice';
import { getLanguageModelCache } from './languageModelCache';
import { formatError, runSafe } from './utils/errors';
import uri from 'vscode-uri';
import URI from 'vscode-uri';
import { getPathCompletionParticipant } from './pathCompletion';

export interface Settings {
css: LanguageSettings;
Expand Down Expand Up @@ -57,7 +58,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
if (!Array.isArray(workspaceFolders)) {
workspaceFolders = [];
if (params.rootPath) {
workspaceFolders.push({ name: '', uri: uri.file(params.rootPath).toString() });
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
}
}

Expand Down Expand Up @@ -181,7 +182,17 @@ function validateTextDocument(textDocument: TextDocument): void {
connection.onCompletion(textDocumentPosition => {
return runSafe(() => {
let document = documents.get(textDocumentPosition.textDocument.uri);
return getLanguageService(document).doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */
const cssLS = getLanguageService(document);
const pathCompletionList: CompletionList = {
isIncomplete: false,
items: []
};
cssLS.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, pathCompletionList)]);
const result = cssLS.doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */
return {
isIncomplete: result.isIncomplete,
items: [...pathCompletionList.items, ...result.items]
};
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`);
});

Expand Down
119 changes: 119 additions & 0 deletions extensions/css/server/src/pathCompletion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import * as path from 'path';
import * as fs from 'fs';
import URI from 'vscode-uri';

import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types';
import { WorkspaceFolder } from 'vscode-languageserver';
import { ICompletionParticipant } from 'vscode-css-languageservice';

import { startsWith } from './utils/strings';

export function getPathCompletionParticipant(
document: TextDocument,
workspaceFolders: WorkspaceFolder[] | undefined,
result: CompletionList
): ICompletionParticipant {
return {
onURILiteralValue: (context: { uriValue: string, position: Position, range: Range; }) => {
if (!workspaceFolders || workspaceFolders.length === 0) {
return;
}
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);

// Handle quoted values
let uriValue = context.uriValue;
let range = context.range;
if (startsWith(uriValue, `'`) || startsWith(uriValue, `"`)) {
uriValue = uriValue.slice(1, -1);
range = getRangeWithoutQuotes(range);
}

const suggestions = providePathSuggestions(uriValue, range, URI.parse(document.uri).fsPath, workspaceRoot);
result.items = [...suggestions, ...result.items];
}
};
}

export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] {
if (startsWith(value, '/') && !root) {
return [];
}

let replaceRange: Range;
const lastIndexOfSlash = value.lastIndexOf('/');
if (lastIndexOfSlash === -1) {
replaceRange = getFullReplaceRange(range);
} else {
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
replaceRange = getReplaceRange(range, valueAfterLastSlash);
}

let parentDir: string;
if (lastIndexOfSlash === -1) {
parentDir = path.resolve(root);
} else {
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);

parentDir = startsWith(value, '/')
? path.resolve(root, '.' + valueBeforeLastSlash)
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
}

try {
return fs.readdirSync(parentDir).map(f => {
if (isDir(path.resolve(parentDir, f))) {
return {
label: f + '/',
kind: CompletionItemKind.Folder,
textEdit: TextEdit.replace(replaceRange, f + '/'),
command: {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
}
};
} else {
return {
label: f,
kind: CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, f)
};
}
});
} catch (e) {
return [];
}
}

const isDir = (p: string) => {
return fs.statSync(p).isDirectory();
};

function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined {
for (let i = 0; i < workspaceFolders.length; i++) {
if (startsWith(activeDoc.uri, workspaceFolders[i].uri)) {
return path.resolve(URI.parse(workspaceFolders[i].uri).fsPath);
}
}
}

function getFullReplaceRange(valueRange: Range) {
const start = Position.create(valueRange.end.line, valueRange.start.character);
const end = Position.create(valueRange.end.line, valueRange.end.character);
return Range.create(start, end);
}
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) {
const start = Position.create(valueRange.end.line, valueRange.end.character - valueAfterLastSlash.length);
const end = Position.create(valueRange.end.line, valueRange.end.character);
return Range.create(start, end);
}
function getRangeWithoutQuotes(range: Range) {
const start = Position.create(range.start.line, range.start.character + 1);
const end = Position.create(range.end.line, range.end.character - 1);
return Range.create(start, end);
}
81 changes: 81 additions & 0 deletions extensions/css/server/src/test/completion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import 'mocha';
import * as assert from 'assert';
import * as path from 'path';
import Uri from 'vscode-uri';
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
import { WorkspaceFolder } from 'vscode-languageserver-protocol';
import { getPathCompletionParticipant } from '../pathCompletion';
import { getCSSLanguageService } from 'vscode-css-languageservice';

export interface ItemDescription {
label: string;
resultText?: string;
}

suite('Completions', () => {
const cssLanguageService = getCSSLanguageService();

let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document: TextDocument, offset: number) {
let matches = completions.items.filter(completion => {
return completion.label === expected.label;
});

assert.equal(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`);
let match = matches[0];
if (expected.resultText && match.textEdit) {
assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText);
}
};

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

const document = TextDocument.create(testUri, 'css', 0, value);
const position = document.positionAt(offset);

if (!workspaceFolders) {
workspaceFolders = [{ name: 'x', uri: path.dirname(testUri) }];
}

let participantResult = CompletionList.create([]);
cssLanguageService.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, participantResult)]);

const stylesheet = cssLanguageService.parseStylesheet(document);
let list = cssLanguageService.doComplete!(document, position, stylesheet);
list.items = list.items.concat(participantResult.items);

if (expected.count) {
assert.equal(list.items.length, expected.count);
}
if (expected.items) {
for (let item of expected.items) {
assertCompletion(list, item, document, offset);
}
}
}

test('CSS Path completion', function () {
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).fsPath;

assertCompletions('html { background-image: url("./|")', {
items: [
{ label: 'about.html', resultText: 'html { background-image: url("./about.html")' }
]
}, testUri);

assertCompletions(`html { background-image: url('../|')`, {
items: [
{ label: 'about/', resultText: `html { background-image: url('../about/')` },
{ label: 'index.html', resultText: `html { background-image: url('../index.html')` },
{ label: 'src/', resultText: `html { background-image: url('../src/')` }
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also test:
url('../a|')
url('../a|b')
url(a|)

}, testUri);
});
});
2 changes: 1 addition & 1 deletion extensions/css/server/src/test/emmet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import 'mocha';
import * as assert from 'assert';
import { getCSSLanguageService, getSCSSLanguageService } from 'vscode-css-languageservice';
import { getCSSLanguageService, getSCSSLanguageService } from 'vscode-css-languageservice/lib/umd/cssLanguageService';
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
import { getEmmetCompletionParticipants } from 'vscode-emmet-helper';

Expand Down
19 changes: 19 additions & 0 deletions extensions/css/server/src/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only copy the functions you need

* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

export function startsWith(haystack: string, needle: string): boolean {
if (haystack.length < needle.length) {
return false;
}

for (let i = 0; i < needle.length; i++) {
if (haystack[i] !== needle[i]) {
return false;
}
}

return true;
}
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.
*--------------------------------------------------------------------------------------------*/
Empty file.
Empty file.
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.
*--------------------------------------------------------------------------------------------*/
4 changes: 4 additions & 0 deletions extensions/css/server/test/pathCompletionFixtures/src/test.js
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
Expand Up @@ -8,7 +8,7 @@ import 'mocha';
import * as assert from 'assert';
import * as path from 'path';
import Uri from 'vscode-uri';
import { TextDocument, CompletionList, CompletionItemKind, } from 'vscode-languageserver-types';
import { TextDocument, CompletionList, CompletionItemKind } from 'vscode-languageserver-types';
import { getLanguageModes } from '../modes/languageModes';
import { getPathCompletionParticipant } from '../modes/pathCompletion';
import { WorkspaceFolder } from 'vscode-languageserver';
Expand Down