import vscodeIconsManifest from "./vscode-icons-manifest.json";import languageAssociationsData from "./vscode-icons-language-associations.json"; const VSCODE_ICONS_VERSION = "v12.17.0";const VSCODE_ICONS_BASE_URL = `https://cdn.jsdelivr.net/gh/vscode-icons/vscode-icons@${VSCODE_ICONS_VERSION}/icons`; interface IconDefinition { iconPath: string;} interface IconLookupSection { file?: string; folder?: string; fileNames: Record<string, string>; fileExtensions: Record<string, string>; folderNames: Record<string, string>; languageIds?: Record<string, string>;} interface VscodeIconsManifest extends IconLookupSection { iconDefinitions: Record<string, IconDefinition>; light: IconLookupSection;} interface LanguageAssociations { version: string; extensionToLanguageId: Record<string, string>; fileNameToLanguageId: Record<string, string>;} const manifest = vscodeIconsManifest as VscodeIconsManifest;const languageAssociations = languageAssociationsData as LanguageAssociations;const iconDefinitions = manifest.iconDefinitions; const darkFileNames = toLowercaseLookup(manifest.fileNames);const lightFileNames = toLowercaseLookup(manifest.light.fileNames);const darkFileExtensions = toLowercaseLookup(manifest.fileExtensions);const lightFileExtensions = toLowercaseLookup(manifest.light.fileExtensions);const darkFolderNames = toLowercaseLookup(manifest.folderNames);const lightFolderNames = toLowercaseLookup(manifest.light.folderNames);const darkLanguageIds = toLowercaseLookup(manifest.languageIds ?? {});const lightLanguageIds = toLowercaseLookup(manifest.light.languageIds ?? {});const languageIdByExtension = toLowercaseLookup(languageAssociations.extensionToLanguageId);const languageIdByFileName = toLowercaseLookup(languageAssociations.fileNameToLanguageId);const localLanguageIdByExtensionOverrides = { // Cursor rules files (*.mdc) are commonly treated as markdown in VSCode/Cursor. mdc: "markdown", // Upstream languages.ts currently maps .html to django-html before html. // Prefer the base HTML icon for standalone HTML files. html: "html", // Upstream languages.ts maps yml/yaml to specialized language ids that can produce // non-generic YAML icons (for example cloudfoundry/esphome). Prefer the base YAML icon // unless a more specific basename/extension match (e.g. azure-pipelines.yml) is found. yml: "yaml", yaml: "yaml",} as const; const defaultDarkFileIconDefinition = manifest.file ?? "_file";const defaultLightFileIconDefinition = manifest.light.file ?? defaultDarkFileIconDefinition;const defaultDarkFolderIconDefinition = manifest.folder ?? "_folder";const defaultLightFolderIconDefinition = manifest.light.folder ?? defaultDarkFolderIconDefinition; function toLowercaseLookup(source: Record<string, string>): Record<string, string> { const entries = Object.entries(source); const lookup: Record<string, string> = {}; for (const [key, value] of entries) { lookup[key.toLowerCase()] = value; } return lookup;} export function basenameOfPath(pathValue: string): string { const slashIndex = pathValue.lastIndexOf("/"); if (slashIndex === -1) return pathValue; return pathValue.slice(slashIndex + 1);} function extensionCandidates(fileName: string): string[] { const candidates = new Set<string>(); if (fileName.includes(".")) { candidates.add(fileName); } let dotIndex = fileName.indexOf("."); while (dotIndex !== -1 && dotIndex < fileName.length - 1) { const candidate = fileName.slice(dotIndex + 1); if (candidate.length > 0) { candidates.add(candidate); } dotIndex = fileName.indexOf(".", dotIndex + 1); } return [...candidates];} function resolveLanguageFallbackDefinition( pathValue: string, theme: "light" | "dark",): string | null { const basename = basenameOfPath(pathValue).toLowerCase(); const languageIds = theme === "light" ? lightLanguageIds : darkLanguageIds; const fromBasenameLanguage = languageIdByFileName[basename]; if (fromBasenameLanguage) { return languageIds[fromBasenameLanguage] ?? darkLanguageIds[fromBasenameLanguage] ?? null; } for (const candidate of extensionCandidates(basename)) { const languageId = localLanguageIdByExtensionOverrides[ candidate as keyof typeof localLanguageIdByExtensionOverrides ] ?? languageIdByExtension[candidate]; if (!languageId) continue; return languageIds[languageId] ?? darkLanguageIds[languageId] ?? null; } return null;} function iconFilenameForDefinitionKey(definitionKey: string | undefined): string | null { if (!definitionKey) return null; const iconPath = iconDefinitions[definitionKey]?.iconPath; if (!iconPath) return null; const slashIndex = iconPath.lastIndexOf("/"); if (slashIndex === -1) { return iconPath; } return iconPath.slice(slashIndex + 1);} function resolveFileDefinition(pathValue: string, theme: "light" | "dark"): string { const basename = basenameOfPath(pathValue).toLowerCase(); const fileNames = theme === "light" ? lightFileNames : darkFileNames; const fileExtensions = theme === "light" ? lightFileExtensions : darkFileExtensions; const fromFileName = fileNames[basename] ?? darkFileNames[basename]; if (fromFileName) return fromFileName; for (const candidate of extensionCandidates(basename)) { const fromExtension = fileExtensions[candidate] ?? darkFileExtensions[candidate]; if (fromExtension) return fromExtension; } const fromLanguage = resolveLanguageFallbackDefinition(pathValue, theme); if (fromLanguage) return fromLanguage; return theme === "light" ? defaultLightFileIconDefinition : defaultDarkFileIconDefinition;} function resolveFolderDefinition(pathValue: string, theme: "light" | "dark"): string { const basename = basenameOfPath(pathValue).toLowerCase(); const folderNames = theme === "light" ? lightFolderNames : darkFolderNames; return ( folderNames[basename] ?? darkFolderNames[basename] ?? (theme === "light" ? defaultLightFolderIconDefinition : defaultDarkFolderIconDefinition) );} export function getVscodeIconUrlForEntry( pathValue: string, kind: "file" | "directory", theme: "light" | "dark",): string { const definitionKey = kind === "directory" ? resolveFolderDefinition(pathValue, theme) : resolveFileDefinition(pathValue, theme); const iconFilename = iconFilenameForDefinitionKey(definitionKey) ?? (kind === "directory" ? "default_folder.svg" : "default_file.svg"); return `${VSCODE_ICONS_BASE_URL}/${iconFilename}`;}
import vscodeIconsManifest from "./vscode-icons-manifest.json";
import languageAssociationsData from "./vscode-icons-language-associations.json";
const VSCODE_ICONS_VERSION = "v12.17.0";
const VSCODE_ICONS_BASE_URL = `https://cdn.jsdelivr.net/gh/vscode-icons/vscode-icons@${VSCODE_ICONS_VERSION}/icons`;
interface IconDefinition {
iconPath: string;
}
interface IconLookupSection {
file?: string;
folder?: string;
fileNames: Record<string, string>;
fileExtensions: Record<string, string>;
folderNames: Record<string, string>;
languageIds?: Record<string, string>;
}
interface VscodeIconsManifest extends IconLookupSection {
iconDefinitions: Record<string, IconDefinition>;
light: IconLookupSection;
}
interface LanguageAssociations {
version: string;
extensionToLanguageId: Record<string, string>;
fileNameToLanguageId: Record<string, string>;
}
const manifest = vscodeIconsManifest as VscodeIconsManifest;
const languageAssociations = languageAssociationsData as LanguageAssociations;
const iconDefinitions = manifest.iconDefinitions;
const darkFileNames = toLowercaseLookup(manifest.fileNames);
const lightFileNames = toLowercaseLookup(manifest.light.fileNames);
const darkFileExtensions = toLowercaseLookup(manifest.fileExtensions);
const lightFileExtensions = toLowercaseLookup(manifest.light.fileExtensions);
const darkFolderNames = toLowercaseLookup(manifest.folderNames);
const lightFolderNames = toLowercaseLookup(manifest.light.folderNames);
const darkLanguageIds = toLowercaseLookup(manifest.languageIds ?? {});
const lightLanguageIds = toLowercaseLookup(manifest.light.languageIds ?? {});
const languageIdByExtension = toLowercaseLookup(languageAssociations.extensionToLanguageId);
const languageIdByFileName = toLowercaseLookup(languageAssociations.fileNameToLanguageId);
const localLanguageIdByExtensionOverrides = {
// Cursor rules files (*.mdc) are commonly treated as markdown in VSCode/Cursor.
mdc: "markdown",
// Upstream languages.ts currently maps .html to django-html before html.
// Prefer the base HTML icon for standalone HTML files.
html: "html",
// Upstream languages.ts maps yml/yaml to specialized language ids that can produce
// non-generic YAML icons (for example cloudfoundry/esphome). Prefer the base YAML icon
// unless a more specific basename/extension match (e.g. azure-pipelines.yml) is found.
yml: "yaml",
yaml: "yaml",
} as const;
const defaultDarkFileIconDefinition = manifest.file ?? "_file";
const defaultLightFileIconDefinition = manifest.light.file ?? defaultDarkFileIconDefinition;
const defaultDarkFolderIconDefinition = manifest.folder ?? "_folder";
const defaultLightFolderIconDefinition = manifest.light.folder ?? defaultDarkFolderIconDefinition;
function toLowercaseLookup(source: Record<string, string>): Record<string, string> {
const entries = Object.entries(source);
const lookup: Record<string, string> = {};
for (const [key, value] of entries) {
lookup[key.toLowerCase()] = value;
}
return lookup;
}
export function basenameOfPath(pathValue: string): string {
const slashIndex = pathValue.lastIndexOf("/");
if (slashIndex === -1) return pathValue;
return pathValue.slice(slashIndex + 1);
}
function extensionCandidates(fileName: string): string[] {
const candidates = new Set<string>();
if (fileName.includes(".")) {
candidates.add(fileName);
}
let dotIndex = fileName.indexOf(".");
while (dotIndex !== -1 && dotIndex < fileName.length - 1) {
const candidate = fileName.slice(dotIndex + 1);
if (candidate.length > 0) {
candidates.add(candidate);
}
dotIndex = fileName.indexOf(".", dotIndex + 1);
}
return [...candidates];
}
function resolveLanguageFallbackDefinition(
pathValue: string,
theme: "light" | "dark",
): string | null {
const basename = basenameOfPath(pathValue).toLowerCase();
const languageIds = theme === "light" ? lightLanguageIds : darkLanguageIds;
const fromBasenameLanguage = languageIdByFileName[basename];
if (fromBasenameLanguage) {
return languageIds[fromBasenameLanguage] ?? darkLanguageIds[fromBasenameLanguage] ?? null;
}
for (const candidate of extensionCandidates(basename)) {
const languageId =
localLanguageIdByExtensionOverrides[
candidate as keyof typeof localLanguageIdByExtensionOverrides
] ?? languageIdByExtension[candidate];
if (!languageId) continue;
return languageIds[languageId] ?? darkLanguageIds[languageId] ?? null;
}
return null;
}
function iconFilenameForDefinitionKey(definitionKey: string | undefined): string | null {
if (!definitionKey) return null;
const iconPath = iconDefinitions[definitionKey]?.iconPath;
if (!iconPath) return null;
const slashIndex = iconPath.lastIndexOf("/");
if (slashIndex === -1) {
return iconPath;
}
return iconPath.slice(slashIndex + 1);
}
function resolveFileDefinition(pathValue: string, theme: "light" | "dark"): string {
const basename = basenameOfPath(pathValue).toLowerCase();
const fileNames = theme === "light" ? lightFileNames : darkFileNames;
const fileExtensions = theme === "light" ? lightFileExtensions : darkFileExtensions;
const fromFileName = fileNames[basename] ?? darkFileNames[basename];
if (fromFileName) return fromFileName;
for (const candidate of extensionCandidates(basename)) {
const fromExtension = fileExtensions[candidate] ?? darkFileExtensions[candidate];
if (fromExtension) return fromExtension;
}
const fromLanguage = resolveLanguageFallbackDefinition(pathValue, theme);
if (fromLanguage) return fromLanguage;
return theme === "light" ? defaultLightFileIconDefinition : defaultDarkFileIconDefinition;
}
function resolveFolderDefinition(pathValue: string, theme: "light" | "dark"): string {
const basename = basenameOfPath(pathValue).toLowerCase();
const folderNames = theme === "light" ? lightFolderNames : darkFolderNames;
return (
folderNames[basename] ??
darkFolderNames[basename] ??
(theme === "light" ? defaultLightFolderIconDefinition : defaultDarkFolderIconDefinition)
);
}
export function getVscodeIconUrlForEntry(
pathValue: string,
kind: "file" | "directory",
theme: "light" | "dark",
): string {
const definitionKey =
kind === "directory"
? resolveFolderDefinition(pathValue, theme)
: resolveFileDefinition(pathValue, theme);
const iconFilename =
iconFilenameForDefinitionKey(definitionKey) ??
(kind === "directory" ? "default_folder.svg" : "default_file.svg");
return `${VSCODE_ICONS_BASE_URL}/${iconFilename}`;
}