diff --git a/src/htmlLanguageTypes.ts b/src/htmlLanguageTypes.ts index d12ada7..83d4b08 100644 --- a/src/htmlLanguageTypes.ts +++ b/src/htmlLanguageTypes.ts @@ -139,6 +139,7 @@ export interface HtmlAttributeValueContext { attribute: string; value: string; range: Range; + attributes?: { [name: string]: string | null }; } export interface HtmlContentContext { diff --git a/src/services/htmlCompletion.ts b/src/services/htmlCompletion.ts index 11b9834..94fc75c 100644 --- a/src/services/htmlCompletion.ts +++ b/src/services/htmlCompletion.ts @@ -307,18 +307,17 @@ export class HTMLCompletion { addQuotes = true; } - if (completionParticipants.length > 0) { - const tag = currentTag.toLowerCase(); - const attribute = currentAttributeName.toLowerCase(); - const fullRange = getReplaceRange(valueStart, valueEnd); - for (const participant of completionParticipants) { - if (participant.onHtmlAttributeValue) { - participant.onHtmlAttributeValue({ document, position, tag, attribute, value: valuePrefix, range: fullRange }); - } + if (completionParticipants.length > 0) { + const tag = currentTag.toLowerCase(); + const attribute = currentAttributeName.toLowerCase(); + const fullRange = getReplaceRange(valueStart, valueEnd); + for (const participant of completionParticipants) { + if (participant.onHtmlAttributeValue) { + participant.onHtmlAttributeValue({ document, position, tag, attribute, value: valuePrefix, range: fullRange, attributes: node.attributes }); } } - - dataProviders.forEach(provider => { + } + dataProviders.forEach(provider => { provider.provideValues(currentTag, currentAttributeName).forEach(value => { const insertText = addQuotes ? '"' + value.name + '"' : value.name; diff --git a/src/services/pathCompletion.ts b/src/services/pathCompletion.ts index 1739d33..e79074a 100644 --- a/src/services/pathCompletion.ts +++ b/src/services/pathCompletion.ts @@ -28,7 +28,7 @@ export class PathCompletionParticipant implements ICompletionParticipant { result.isIncomplete = true; } else { const replaceRange = pathToReplaceRange(attributeCompletion.value, fullValue, attributeCompletion.range); - const suggestions = await this.providePathSuggestions(attributeCompletion.value, replaceRange, document, documentContext); + const suggestions = await this.providePathSuggestions(attributeCompletion.value, replaceRange, document, documentContext, attributeCompletion); for (const item of suggestions) { result.items.push(item); } @@ -38,7 +38,7 @@ export class PathCompletionParticipant implements ICompletionParticipant { return result; } - private async providePathSuggestions(valueBeforeCursor: string, replaceRange: Range, document: TextDocument, documentContext: DocumentContext) { + private async providePathSuggestions(valueBeforeCursor: string, replaceRange: Range, document: TextDocument, documentContext: DocumentContext, context?: HtmlAttributeValueContext) { const valueBeforeLastSlash = valueBeforeCursor.substring(0, valueBeforeCursor.lastIndexOf('/') + 1); // keep the last slash let parentDir = documentContext.resolveReference(valueBeforeLastSlash || '.', document.uri); @@ -46,10 +46,37 @@ export class PathCompletionParticipant implements ICompletionParticipant { try { const result: CompletionItem[] = []; 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) { - result.push(createCompletionItem(name, type === FileType.Directory, replaceRange)); + 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; @@ -60,6 +87,51 @@ export class PathCompletionParticipant implements ICompletionParticipant { return []; } + /** + * Determines which file extensions to filter/prioritize based on the HTML tag and attributes + */ + private getExtensionFilter(context?: HtmlAttributeValueContext): { extensions: string[], exclusive: boolean } | undefined { + 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