| 1 | import type { CSSProperties } from "react"; |
| 2 | import setiTheme from "../assets/file-icons/seti/vs-seti-icon-theme.json"; |
| 3 | |
| 4 | type IconDefinition = { fontCharacter?: string; fontColor?: string }; |
| 5 | type IconAssociations = { |
| 6 | file?: string; |
| 7 | fileExtensions?: Record<string, string>; |
| 8 | fileNames?: Record<string, string>; |
| 9 | languageIds?: Record<string, string>; |
| 10 | }; |
| 11 | |
| 12 | const theme = setiTheme as typeof setiTheme & IconAssociations & { |
| 13 | iconDefinitions: Record<string, IconDefinition>; |
| 14 | light?: IconAssociations; |
| 15 | }; |
| 16 | |
| 17 | const languageIdByExtension: Record<string, string> = { |
| 18 | bash: "shellscript", |
| 19 | c: "c", |
| 20 | cc: "cpp", |
| 21 | cpp: "cpp", |
| 22 | cs: "csharp", |
| 23 | css: "css", |
| 24 | dart: "dart", |
| 25 | dockerfile: "dockerfile", |
| 26 | go: "go", |
| 27 | gradle: "groovy", |
| 28 | h: "c", |
| 29 | hpp: "cpp", |
| 30 | html: "html", |
| 31 | java: "java", |
| 32 | js: "javascript", |
| 33 | json: "json", |
| 34 | jsonl: "jsonl", |
| 35 | jsx: "javascriptreact", |
| 36 | kt: "kotlin", |
| 37 | kts: "kotlin", |
| 38 | less: "less", |
| 39 | lua: "lua", |
| 40 | md: "markdown", |
| 41 | markdown: "markdown", |
| 42 | php: "php", |
| 43 | properties: "properties", |
| 44 | ps1: "powershell", |
| 45 | py: "python", |
| 46 | rb: "ruby", |
| 47 | rs: "rust", |
| 48 | scss: "scss", |
| 49 | sh: "shellscript", |
| 50 | sql: "sql", |
| 51 | svelte: "svelte", |
| 52 | swift: "swift", |
| 53 | toml: "toml", |
| 54 | ts: "typescript", |
| 55 | tsx: "typescriptreact", |
| 56 | vue: "vue", |
| 57 | xml: "xml", |
| 58 | yaml: "yaml", |
| 59 | yml: "yaml", |
| 60 | }; |
| 61 | |
| 62 | function extensionCandidates(fileName: string): string[] { |
| 63 | const parts = fileName.split("."); |
| 64 | if (parts.length < 2) return [fileName]; |
| 65 | const candidates: string[] = []; |
| 66 | for (let index = 1; index < parts.length; index += 1) { |
| 67 | candidates.push(parts.slice(index).join(".")); |
| 68 | } |
| 69 | return candidates; |
| 70 | } |
| 71 | |
| 72 | function associationFor(fileName: string, associations: IconAssociations | undefined): string | undefined { |
| 73 | if (!associations) return undefined; |
| 74 | const normalized = fileName.toLowerCase(); |
| 75 | const exact = associations.fileNames?.[normalized]; |
| 76 | if (exact) return exact; |
| 77 | for (const extension of extensionCandidates(normalized)) { |
| 78 | const icon = associations.fileExtensions?.[extension]; |
| 79 | if (icon) return icon; |
| 80 | } |
| 81 | const finalExtension = normalized.includes(".") ? normalized.slice(normalized.lastIndexOf(".") + 1) : normalized; |
| 82 | const languageId = languageIdByExtension[finalExtension]; |
| 83 | return (languageId ? associations.languageIds?.[languageId] : undefined) ?? associations.file; |
| 84 | } |
| 85 | |
| 86 | function glyphFor(definition: IconDefinition | undefined): string { |
| 87 | const cssEscape = definition?.fontCharacter; |
| 88 | if (!cssEscape?.startsWith("\\")) return ""; |
| 89 | const codePoint = Number.parseInt(cssEscape.slice(1), 16); |
| 90 | return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ""; |
| 91 | } |
| 92 | |
| 93 | export function workspaceFileIcon(fileName: string): { |
| 94 | glyph: string; |
| 95 | darkColor: string; |
| 96 | lightColor: string; |
| 97 | } { |
| 98 | const darkId = associationFor(fileName, theme) ?? theme.file ?? "_default"; |
| 99 | const lightId = associationFor(fileName, theme.light) ?? `${darkId}_light`; |
| 100 | const darkDefinition = theme.iconDefinitions[darkId] ?? theme.iconDefinitions._default; |
| 101 | const lightDefinition = theme.iconDefinitions[lightId] ?? darkDefinition; |
| 102 | return { |
| 103 | glyph: glyphFor(darkDefinition), |
| 104 | darkColor: darkDefinition?.fontColor ?? "currentColor", |
| 105 | lightColor: lightDefinition?.fontColor ?? darkDefinition?.fontColor ?? "currentColor", |
| 106 | }; |
| 107 | } |
| 108 | |
| 109 | export function WorkspaceFileIcon({ fileName, className = "" }: { fileName: string; className?: string }) { |
| 110 | const icon = workspaceFileIcon(fileName); |
| 111 | const style = { |
| 112 | "--workspace-file-icon-dark": icon.darkColor, |
| 113 | "--workspace-file-icon-light": icon.lightColor, |
| 114 | } as CSSProperties; |
| 115 | return ( |
| 116 | <span |
| 117 | aria-hidden="true" |
| 118 | className={`workspace-file-icon${className ? ` ${className}` : ""}`} |
| 119 | style={style} |
| 120 | > |
| 121 | {icon.glyph} |
| 122 | </span> |
| 123 | ); |
| 124 | } |
| 125 |