mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-04 15:25:47 +01:00
path completion for css. fix #45235
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user