/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CompletionItemKind, TextEdit, Range, Position, FileType } from '../htmlLanguageTypes'; import { startsWith } from '../utils/strings'; export class PathCompletionParticipant { constructor(dataManager, readDirectory) { this.dataManager = dataManager; this.readDirectory = readDirectory; this.atributeCompletions = []; } onHtmlAttributeValue(context) { if (this.dataManager.isPathAttribute(context.tag, context.attribute)) { this.atributeCompletions.push(context); } } async computeCompletions(document, documentContext) { const result = { items: [], isIncomplete: false }; for (const attributeCompletion of this.atributeCompletions) { const fullValue = stripQuotes(document.getText(attributeCompletion.range)); if (isCompletablePath(fullValue)) { if (fullValue === '.' || fullValue === '..') { result.isIncomplete = true; } else { const replaceRange = pathToReplaceRange(attributeCompletion.value, fullValue, attributeCompletion.range); const suggestions = await this.providePathSuggestions(attributeCompletion.value, replaceRange, document, documentContext, attributeCompletion); for (const item of suggestions) { result.items.push(item); } } } } return result; } async providePathSuggestions(valueBeforeCursor, replaceRange, document, documentContext, context) { const valueBeforeLastSlash = valueBeforeCursor.substring(0, valueBeforeCursor.lastIndexOf('/') + 1); // keep the last slash let parentDir = documentContext.resolveReference(valueBeforeLastSlash || '.', document.uri); if (parentDir) { try { const result = []; const infos = await this.readDirectory(parentDir); // Determine file extensions to prioritize/filter based on tag and attributes const extensionFilter = this.getExtensionFilter(context); for (const [name, type] of infos) { // Exclude paths that start with `.` if (name.charCodeAt(0) !== CharCode_dot) { const item = createCompletionItem(name, type === FileType.Directory, replaceRange); // Apply filtering/sorting based on file extension if (extensionFilter) { if (type === FileType.Directory) { // Always include directories result.push(item); } else { // For files, check if they match the filter const matchesFilter = extensionFilter.extensions.some(ext => name.toLowerCase().endsWith(ext)); if (matchesFilter) { // Add matching files with higher sort priority item.sortText = '0_' + name; result.push(item); } else if (!extensionFilter.exclusive) { // Add non-matching files with lower sort priority if not exclusive item.sortText = '1_' + name; result.push(item); } // If exclusive and doesn't match, don't add the file } } else { result.push(item); } } } return result; } catch (e) { // ignore } } return []; } /** * Determines which file extensions to filter/prioritize based on the HTML tag and attributes */ getExtensionFilter(context) { if (!context) { return undefined; } // Handle tag with rel="stylesheet" if (context.tag === 'link' && context.attribute === 'href' && context.attributes) { const rel = context.attributes['rel']; if (rel === 'stylesheet' || rel === '"stylesheet"' || rel === "'stylesheet'") { // Filter to CSS files for stylesheets return { extensions: ['.css', '.scss', '.sass', '.less'], exclusive: false }; } if (rel === 'icon' || rel === '"icon"' || rel === "'icon'" || rel === 'apple-touch-icon' || rel === '"apple-touch-icon"' || rel === "'apple-touch-icon'") { // Filter to image files for icons return { extensions: ['.ico', '.png', '.svg', '.jpg', '.jpeg', '.gif', '.webp'], exclusive: false }; } } // Handle