path completion for css. fix #45235

This commit is contained in:
Pine Wu 2018-03-14 11:05:37 -07:00
parent 7192b6a540
commit 68ae0e5aa6
5 changed files with 205 additions and 4 deletions

View file

@ -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",
@ -32,7 +38,8 @@
"protocol": "inspector",
"port": 6044,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/server/out/**/*.js"]
"outFiles": ["${workspaceFolder}/server/out/**/*.js"],
"restart": true
}
]
}

View file

@ -8,7 +8,7 @@ import {
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities
} from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-types';
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
import { ConfigurationRequest } from 'vscode-languageserver-protocol/lib/protocol.configuration.proposed';
import { WorkspaceFolder } from 'vscode-languageserver-protocol/lib/protocol.workspaceFolders.proposed';
@ -18,6 +18,7 @@ import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService,
import { getLanguageModelCache } from './languageModelCache';
import { formatError, runSafe } from './utils/errors';
import uri from 'vscode-uri';
import { getPathCompletionParticipant } from './pathCompletion';
export interface Settings {
css: LanguageSettings;
@ -184,7 +185,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 = getLanguageService(document).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}`);
});

View file

@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* 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 { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types';
import { Proposed } from 'vscode-languageserver-protocol';
import * as path from 'path';
import * as fs from 'fs';
import URI from 'vscode-uri';
import { ICompletionParticipant } from 'vscode-css-languageservice/lib/umd/cssLanguageService';
import { startsWith } from './utils/strings';
export function getPathCompletionParticipant(
document: TextDocument,
workspaceFolders: Proposed.WorkspaceFolder[] | undefined,
result: CompletionList
): ICompletionParticipant {
return {
onCssURILiteralValue: (context: { uriValue: string, position: Position, range: Range; }) => {
if (!workspaceFolders || workspaceFolders.length === 0) {
return;
}
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
const suggestions = providePathSuggestions(context.uriValue, context.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: Proposed.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);
}

View file

@ -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';

View file

@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* 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 getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number, length: number } {
let lineStart = offset;
while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) {
lineStart--;
}
let offsetInLine = offset - lineStart;
let lineText = text.substr(lineStart);
// make a copy of the regex as to not keep the state
let flags = wordDefinition.ignoreCase ? 'gi' : 'g';
wordDefinition = new RegExp(wordDefinition.source, flags);
let match = wordDefinition.exec(lineText);
while (match && match.index + match[0].length < offsetInLine) {
match = wordDefinition.exec(lineText);
}
if (match && match.index <= offsetInLine) {
return { start: match.index + lineStart, length: match[0].length };
}
return { start: offset, length: 0 };
}
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;
}
export function endsWith(haystack: string, needle: string): boolean {
let diff = haystack.length - needle.length;
if (diff > 0) {
return haystack.indexOf(needle, diff) === diff;
} else if (diff === 0) {
return haystack === needle;
} else {
return false;
}
}
export function repeat(value: string, count: number) {
var s = '';
while (count > 0) {
if ((count & 1) === 1) {
s += value;
}
value += value;
count = count >>> 1;
}
return s;
}
export function isWhitespaceOnly(str: string) {
return /^\s*$/.test(str);
}
export function isEOL(content: string, offset: number) {
return isNewlineCharacter(content.charCodeAt(offset));
}
const CR = '\r'.charCodeAt(0);
const NL = '\n'.charCodeAt(0);
export function isNewlineCharacter(charCode: number) {
return charCode === CR || charCode === NL;
}