Skip to content

Commit 68ae0e5

Browse files
committed
path completion for css. fix microsoft#45235
1 parent 7192b6a commit 68ae0e5

5 files changed

Lines changed: 205 additions & 4 deletions

File tree

extensions/css/.vscode/launch.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"version": "0.2.0",
3+
"compounds": [
4+
{
5+
"name": "Debug Extension and Language Server",
6+
"configurations": ["Launch Extension", "Attach Language Server"]
7+
}
8+
],
39
"configurations": [
410
{
511
"name": "Launch Extension",
@@ -32,7 +38,8 @@
3238
"protocol": "inspector",
3339
"port": 6044,
3440
"sourceMaps": true,
35-
"outFiles": ["${workspaceFolder}/server/out/**/*.js"]
41+
"outFiles": ["${workspaceFolder}/server/out/**/*.js"],
42+
"restart": true
3643
}
3744
]
3845
}

extensions/css/server/src/cssServerMain.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities
99
} from 'vscode-languageserver';
1010

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

1313
import { ConfigurationRequest } from 'vscode-languageserver-protocol/lib/protocol.configuration.proposed';
1414
import { WorkspaceFolder } from 'vscode-languageserver-protocol/lib/protocol.workspaceFolders.proposed';
@@ -18,6 +18,7 @@ import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService,
1818
import { getLanguageModelCache } from './languageModelCache';
1919
import { formatError, runSafe } from './utils/errors';
2020
import uri from 'vscode-uri';
21+
import { getPathCompletionParticipant } from './pathCompletion';
2122

2223
export interface Settings {
2324
css: LanguageSettings;
@@ -184,7 +185,17 @@ function validateTextDocument(textDocument: TextDocument): void {
184185
connection.onCompletion(textDocumentPosition => {
185186
return runSafe(() => {
186187
let document = documents.get(textDocumentPosition.textDocument.uri);
187-
return getLanguageService(document).doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */
188+
const cssLS = getLanguageService(document);
189+
const pathCompletionList: CompletionList = {
190+
isIncomplete: false,
191+
items: []
192+
};
193+
cssLS.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, pathCompletionList)]);
194+
const result = getLanguageService(document).doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */
195+
return {
196+
isIncomplete: result.isIncomplete,
197+
items: [...pathCompletionList.items, ...result.items]
198+
};
188199
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`);
189200
});
190201

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types';
8+
import { Proposed } from 'vscode-languageserver-protocol';
9+
import * as path from 'path';
10+
import * as fs from 'fs';
11+
import URI from 'vscode-uri';
12+
import { ICompletionParticipant } from 'vscode-css-languageservice/lib/umd/cssLanguageService';
13+
import { startsWith } from './utils/strings';
14+
15+
export function getPathCompletionParticipant(
16+
document: TextDocument,
17+
workspaceFolders: Proposed.WorkspaceFolder[] | undefined,
18+
result: CompletionList
19+
): ICompletionParticipant {
20+
return {
21+
onCssURILiteralValue: (context: { uriValue: string, position: Position, range: Range; }) => {
22+
if (!workspaceFolders || workspaceFolders.length === 0) {
23+
return;
24+
}
25+
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
26+
27+
const suggestions = providePathSuggestions(context.uriValue, context.range, URI.parse(document.uri).fsPath, workspaceRoot);
28+
result.items = [...suggestions, ...result.items];
29+
}
30+
};
31+
}
32+
33+
export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] {
34+
if (startsWith(value, '/') && !root) {
35+
return [];
36+
}
37+
38+
let replaceRange: Range;
39+
const lastIndexOfSlash = value.lastIndexOf('/');
40+
if (lastIndexOfSlash === -1) {
41+
replaceRange = getFullReplaceRange(range);
42+
} else {
43+
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
44+
replaceRange = getReplaceRange(range, valueAfterLastSlash);
45+
}
46+
47+
let parentDir: string;
48+
if (lastIndexOfSlash === -1) {
49+
parentDir = path.resolve(root);
50+
} else {
51+
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);
52+
53+
parentDir = startsWith(value, '/')
54+
? path.resolve(root, '.' + valueBeforeLastSlash)
55+
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
56+
}
57+
58+
try {
59+
return fs.readdirSync(parentDir).map(f => {
60+
if (isDir(path.resolve(parentDir, f))) {
61+
return {
62+
label: f + '/',
63+
kind: CompletionItemKind.Folder,
64+
textEdit: TextEdit.replace(replaceRange, f + '/'),
65+
command: {
66+
title: 'Suggest',
67+
command: 'editor.action.triggerSuggest'
68+
}
69+
};
70+
} else {
71+
return {
72+
label: f,
73+
kind: CompletionItemKind.File,
74+
textEdit: TextEdit.replace(replaceRange, f)
75+
};
76+
}
77+
});
78+
} catch (e) {
79+
return [];
80+
}
81+
}
82+
83+
const isDir = (p: string) => {
84+
return fs.statSync(p).isDirectory();
85+
};
86+
87+
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: Proposed.WorkspaceFolder[]): string | undefined {
88+
for (let i = 0; i < workspaceFolders.length; i++) {
89+
if (startsWith(activeDoc.uri, workspaceFolders[i].uri)) {
90+
return path.resolve(URI.parse(workspaceFolders[i].uri).fsPath);
91+
}
92+
}
93+
}
94+
95+
function getFullReplaceRange(valueRange: Range) {
96+
const start = Position.create(valueRange.end.line, valueRange.start.character);
97+
const end = Position.create(valueRange.end.line, valueRange.end.character);
98+
return Range.create(start, end);
99+
}
100+
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) {
101+
const start = Position.create(valueRange.end.line, valueRange.end.character - valueAfterLastSlash.length);
102+
const end = Position.create(valueRange.end.line, valueRange.end.character);
103+
return Range.create(start, end);
104+
}

extensions/css/server/src/test/emmet.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number, length: number } {
8+
let lineStart = offset;
9+
while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) {
10+
lineStart--;
11+
}
12+
let offsetInLine = offset - lineStart;
13+
let lineText = text.substr(lineStart);
14+
15+
// make a copy of the regex as to not keep the state
16+
let flags = wordDefinition.ignoreCase ? 'gi' : 'g';
17+
wordDefinition = new RegExp(wordDefinition.source, flags);
18+
19+
let match = wordDefinition.exec(lineText);
20+
while (match && match.index + match[0].length < offsetInLine) {
21+
match = wordDefinition.exec(lineText);
22+
}
23+
if (match && match.index <= offsetInLine) {
24+
return { start: match.index + lineStart, length: match[0].length };
25+
}
26+
27+
return { start: offset, length: 0 };
28+
}
29+
30+
export function startsWith(haystack: string, needle: string): boolean {
31+
if (haystack.length < needle.length) {
32+
return false;
33+
}
34+
35+
for (let i = 0; i < needle.length; i++) {
36+
if (haystack[i] !== needle[i]) {
37+
return false;
38+
}
39+
}
40+
41+
return true;
42+
}
43+
44+
export function endsWith(haystack: string, needle: string): boolean {
45+
let diff = haystack.length - needle.length;
46+
if (diff > 0) {
47+
return haystack.indexOf(needle, diff) === diff;
48+
} else if (diff === 0) {
49+
return haystack === needle;
50+
} else {
51+
return false;
52+
}
53+
}
54+
55+
export function repeat(value: string, count: number) {
56+
var s = '';
57+
while (count > 0) {
58+
if ((count & 1) === 1) {
59+
s += value;
60+
}
61+
value += value;
62+
count = count >>> 1;
63+
}
64+
return s;
65+
}
66+
67+
export function isWhitespaceOnly(str: string) {
68+
return /^\s*$/.test(str);
69+
}
70+
71+
export function isEOL(content: string, offset: number) {
72+
return isNewlineCharacter(content.charCodeAt(offset));
73+
}
74+
75+
const CR = '\r'.charCodeAt(0);
76+
const NL = '\n'.charCodeAt(0);
77+
export function isNewlineCharacter(charCode: number) {
78+
return charCode === CR || charCode === NL;
79+
}

0 commit comments

Comments
 (0)