/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { WorkspaceFolder } from 'vscode-languageserver'; import * as path from 'path'; import * as fs from 'fs'; import { URI } from 'vscode-uri'; import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-html-languageservice'; import { startsWith } from '../utils/strings'; import { contains } from '../utils/arrays'; export function getPathCompletionParticipant( document: TextDocument, workspaceFolders: WorkspaceFolder[], result: CompletionItem[] ): ICompletionParticipant { return { onHtmlAttributeValue: ({ tag, attribute, value: valueBeforeCursor, range }) => { const fullValue = stripQuotes(document.getText(range)); if (shouldDoPathCompletion(tag, attribute, fullValue)) { if (workspaceFolders.length === 0) { return; } const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot); result.push(...paths.map(p => pathToSuggestion(p, valueBeforeCursor, fullValue, range))); } } }; } function stripQuotes(fullValue: string) { if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { return fullValue.slice(1, -1); } else { return fullValue; } } function shouldDoPathCompletion(tag: string, attr: string, value: string) { if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) { return false; } if (PATH_TAG_AND_ATTR[tag]) { if (typeof PATH_TAG_AND_ATTR[tag] === 'string') { return PATH_TAG_AND_ATTR[tag] === attr; } else { return contains(PATH_TAG_AND_ATTR[tag], attr); } } return false; } /** * 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 { const paths = fs.readdirSync(parentDir).map(f => { return isDir(path.resolve(parentDir, f)) ? f + '/' : f; }); return paths.filter(p => p[0] !== '.'); } catch (e) { return []; } } function isDir(p: string) { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } } function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem { const isDir = p[p.length - 1] === '/'; let replaceRange: Range; const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); if (lastIndexOfSlash === -1) { replaceRange = shiftRange(range, 1, -1); } else { // For cases where cursor is in the middle of attribute value, like