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