Skip to content

Commit

Permalink
Merge pull request #45788 from Microsoft/octref/pathCompletion
Browse files Browse the repository at this point in the history
path completion for css. fix #45235
  • Loading branch information
octref authored Mar 20, 2018
2 parents 2f5061f + d2b9a7c commit 54f1a18
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 7 deletions.
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/')` }
]
}, 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 @@
/*---------------------------------------------------------------------------------------------
* 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

0 comments on commit 54f1a18

Please sign in to comment.