/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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, endsWith } from './utils/strings'; export function getPathCompletionParticipant( document: TextDocument, workspaceFolders: WorkspaceFolder[], result: CompletionList ): ICompletionParticipant { return { onCssURILiteralValue: ({ position, range, uriValue }) => { const fullValue = stripQuotes(uriValue); if (!shouldDoPathCompletion(uriValue, workspaceFolders)) { if (fullValue === '.' || fullValue === '..') { result.isIncomplete = true; } return; } 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; } return; } 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); } } }); } result.items = [...suggestions, ...result.items]; } }; } 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); const currentDocFsPath = URI.parse(document.uri).fsPath; 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] !== '.'; }); 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; } function stripQuotes(fullValue: string) { if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { return fullValue.slice(1, -1); } else { return fullValue; } } /** * 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); const startsWithSlash = startsWith(valueBeforeCursor, '/'); let parentDir: string; if (startsWithSlash) { if (!root) { return []; } parentDir = path.resolve(root, '.' + valueBeforeLastSlash); } else { parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); } try { return fs.readdirSync(parentDir).map(f => { return isDir(path.resolve(parentDir, f)) ? f + '/' : f; }); } catch (e) { return []; } } const isDir = (p: string) => { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } }; function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, fullValueRange: Range) { let replaceRange: Range; const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); if (lastIndexOfSlash === -1) { replaceRange = fullValueRange; } else { // For cases where cursor is in the middle of attribute value, like