pathCompletion.ts 6.7 KB
Newer Older
P
Pine Wu 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
5

P
Pine Wu 已提交
6 7 8
import * as path from 'path';
import * as fs from 'fs';
import URI from 'vscode-uri';
P
Pine Wu 已提交
9 10 11

import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types';
import { WorkspaceFolder } from 'vscode-languageserver';
P
temp  
Pine Wu 已提交
12
import { ICompletionParticipant } from 'vscode-css-languageservice';
P
Pine Wu 已提交
13

14
import { startsWith, endsWith } from './utils/strings';
P
Pine Wu 已提交
15 16 17

export function getPathCompletionParticipant(
	document: TextDocument,
P
temp  
Pine Wu 已提交
18
	workspaceFolders: WorkspaceFolder[],
P
Pine Wu 已提交
19 20 21
	result: CompletionList
): ICompletionParticipant {
	return {
22
		onCssURILiteralValue: ({ position, range, uriValue }) => {
P
temp  
Pine Wu 已提交
23
			const fullValue = stripQuotes(uriValue);
24 25 26 27
			if (!shouldDoPathCompletion(uriValue, workspaceFolders)) {
				if (fullValue === '.' || fullValue === '..') {
					result.isIncomplete = true;
				}
P
Pine Wu 已提交
28 29
				return;
			}
P
temp  
Pine Wu 已提交
30

31 32 33 34 35 36 37 38 39
			let suggestions = providePathSuggestions(uriValue, position, range, document, workspaceFolders);
			result.items = [...suggestions, ...result.items];
		},
		onCssImportPath: ({ position, range, pathValue }) => {
			const fullValue = stripQuotes(pathValue);
			if (!shouldDoPathCompletion(pathValue, workspaceFolders)) {
				if (fullValue === '.' || fullValue === '..') {
					result.isIncomplete = true;
				}
P
Pine Wu 已提交
40
				return;
P
temp  
Pine Wu 已提交
41
			}
P
Pine Wu 已提交
42

43 44 45 46 47 48 49 50 51 52 53 54 55
			let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders);

			if (document.languageId === 'scss') {
				suggestions.forEach(s => {
					if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) {
						if (s.textEdit) {
							s.textEdit.newText = s.label.slice(1, -5);
						} else {
							s.label = s.label.slice(1, -5);
						}
					}
				});
			}
56

P
Pine Wu 已提交
57
			result.items = [...suggestions, ...result.items];
P
Pine Wu 已提交
58 59 60 61
		}
	};
}

62 63 64 65 66 67 68
function providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, workspaceFolders: WorkspaceFolder[]) {
	const fullValue = stripQuotes(pathValue);
	const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`);
	const valueBeforeCursor = isValueQuoted
		? fullValue.slice(0, position.character - (range.start.character + 1))
		: fullValue.slice(0, position.character - range.start.character);
	const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
69 70
	const currentDocFsPath = URI.parse(document.uri).fsPath;

71 72 73 74 75 76 77 78 79
	const paths = providePaths(valueBeforeCursor, currentDocFsPath, workspaceRoot)
		.filter(p => {
			// Exclude current doc's path
			return path.resolve(currentDocFsPath, '../', p) !== currentDocFsPath;
		})
		.filter(p => {
			// Exclude paths that start with `.`
			return p[0] !== '.';
		});
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

	const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
	const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);

	const suggestions = paths.map(p => pathToSuggestion(p, replaceRange));
	return suggestions;
}

function shouldDoPathCompletion(pathValue: string, workspaceFolders: WorkspaceFolder[]): boolean {
	const fullValue = stripQuotes(pathValue);
	if (fullValue === '.' || fullValue === '..') {
		return false;
	}

	if (!workspaceFolders || workspaceFolders.length === 0) {
		return false;
	}

	return true;
}

P
temp  
Pine Wu 已提交
101 102 103 104 105
function stripQuotes(fullValue: string) {
	if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
		return fullValue.slice(1, -1);
	} else {
		return fullValue;
P
Pine Wu 已提交
106
	}
P
temp  
Pine Wu 已提交
107
}
P
Pine Wu 已提交
108

P
temp  
Pine Wu 已提交
109 110 111 112 113 114
/**
 * Get a list of path suggestions. Folder suggestions are suffixed with a slash.
 */
function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] {
	const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
	const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1);
P
Pine Wu 已提交
115

116 117 118 119 120 121 122 123 124 125
	const startsWithSlash = startsWith(valueBeforeCursor, '/');
	let parentDir: string;
	if (startsWithSlash) {
		if (!root) {
			return [];
		}
		parentDir = path.resolve(root, '.' + valueBeforeLastSlash);
	} else {
		parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
	}
P
Pine Wu 已提交
126 127 128

	try {
		return fs.readdirSync(parentDir).map(f => {
P
temp  
Pine Wu 已提交
129 130 131
			return isDir(path.resolve(parentDir, f))
				? f + '/'
				: f;
P
Pine Wu 已提交
132 133 134 135 136 137 138
		});
	} catch (e) {
		return [];
	}
}

const isDir = (p: string) => {
139 140 141 142 143
	try {
		return fs.statSync(p).isDirectory();
	} catch (e) {
		return false;
	}
P
Pine Wu 已提交
144 145
};

P
Pine Wu 已提交
146
function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, fullValueRange: Range) {
P
pi1024e 已提交
147
	let replaceRange: Range;
P
temp  
Pine Wu 已提交
148 149
	const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
	if (lastIndexOfSlash === -1) {
P
pi1024e 已提交
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
		replaceRange = fullValueRange;
	} else {
		// For cases where cursor is in the middle of attribute value, like <script src="./s|rc/test.js">
		// Find the last slash before cursor, and calculate the start of replace range from there
		const valueAfterLastSlash = fullValue.slice(lastIndexOfSlash + 1);
		const startPos = shiftPosition(fullValueRange.end, -valueAfterLastSlash.length);
		// If whitespace exists, replace until it
		const whitespaceIndex = valueAfterLastSlash.indexOf(' ');
		let endPos;
		if (whitespaceIndex !== -1) {
			endPos = shiftPosition(startPos, whitespaceIndex);
		} else {
			endPos = fullValueRange.end;
		}
		replaceRange = Range.create(startPos, endPos);
P
pi1024e 已提交
165
	}
166

P
pi1024e 已提交
167
	return replaceRange;
P
Pine Wu 已提交
168 169 170 171 172
}

function pathToSuggestion(p: string, replaceRange: Range): CompletionItem {
	const isDir = p[p.length - 1] === '/';

P
temp  
Pine Wu 已提交
173 174
	if (isDir) {
		return {
P
Pine Wu 已提交
175
			label: escapePath(p),
P
temp  
Pine Wu 已提交
176
			kind: CompletionItemKind.Folder,
P
Pine Wu 已提交
177
			textEdit: TextEdit.replace(replaceRange, escapePath(p)),
P
temp  
Pine Wu 已提交
178 179 180 181 182
			command: {
				title: 'Suggest',
				command: 'editor.action.triggerSuggest'
			}
		};
P
pi1024e 已提交
183 184 185 186 187 188
	} else {
		return {
			label: escapePath(p),
			kind: CompletionItemKind.File,
			textEdit: TextEdit.replace(replaceRange, escapePath(p))
		};
P
temp  
Pine Wu 已提交
189 190 191
	}
}

P
Pine Wu 已提交
192 193 194 195 196
// Escape https://www.w3.org/TR/CSS1/#url
function escapePath(p: string) {
	return p.replace(/(\s|\(|\)|,|"|')/g, '\\$1');
}

P
Pine Wu 已提交
197
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined {
198 199 200
	for (const folder of workspaceFolders) {
		if (startsWith(activeDoc.uri, folder.uri)) {
			return path.resolve(URI.parse(folder.uri).fsPath);
P
Pine Wu 已提交
201 202
		}
	}
203
	return undefined;
P
Pine Wu 已提交
204 205
}

P
temp  
Pine Wu 已提交
206 207
function shiftPosition(pos: Position, offset: number): Position {
	return Position.create(pos.line, pos.character + offset);
P
Pine Wu 已提交
208
}
P
temp  
Pine Wu 已提交
209 210 211
function shiftRange(range: Range, startOffset: number, endOffset: number): Range {
	const start = shiftPosition(range.start, startOffset);
	const end = shiftPosition(range.end, endOffset);
P
Pine Wu 已提交
212 213
	return Range.create(start, end);
}