jj-dojo / Icon theme service

Icon theme service

How Jujutsu Dojo finds the file-icon theme you picked in VS Code, reads its JSON, and turns a file name into an image or font glyph a webview can display.

On this page
  1. 1. Files
  2. 2. Finding the active theme
  3. 3. Parsing the theme file
  4. IconThemeDocument and IconThemeVariant
  5. IconDefinition
  6. ThemeFont and ThemeFontSrc
  7. 4. The IconTheme model
  8. Resolving a file name
  9. 5. Serving icons to a webview
  10. FileIcon
  11. FileFont
  12. 6. Caching and invalidation
  13. 7. Tests
  14. 8. Loose ends at this pin

In short: IconThemeService.loadTheme reads workbench.iconTheme, finds the extension that contributes that theme, and parses the theme file with jsonc-parser into an IconTheme. getFileIcon(name) then resolves the name in this order: exact file name, the file name's language, the extension, the extension's language, and finally the default icon. The resolved icon definition becomes a vscode-webview:// URI for an image, or a font URI plus glyph and colour for a font icon. The result is cached per theme ID. At this pin nothing outside the directory and its test imports the service, so the commit graph doesn't show file icons yet.

1. Files

FileLinesWhat it holds
icon_theme_json_parser.ts63The theme file's types and a one-line parser.
icon_theme.ts194The IconTheme model: locating the theme, merging the light variant, resolving icon IDs.
icon_theme_service.ts145IconThemeService: the per-webview cache and the FileIcon it returns.
icon_theme_service_test.ts164Two Jasmine tests, one image theme and one font theme.
BUILD.bazel45The ts_library and its jasmine_test.

2. Finding the active theme

loadTheme reads the theme ID from workbench.iconTheme, falling back to vs-seti when the setting is empty (source). IconTheme.load then asks getIconThemeExtension where that theme lives (source):

  1. It walks every installed extension and looks at packageJSON.contributes.iconThemes. An entry matches when its id equals the theme ID, or, for an entry with no id, when its label does. The first match wins and supplies the theme file's path (source).
  2. With no match, it falls back to the built-in vscode.theme-seti extension and the hard-coded path ./icons/vs-seti-icon-theme.json. If even that extension is missing it throws Cannot find built-in VS Code Seti theme (source).

"Every installed extension" is getAllExtensions (source). vscode.extensions.all only lists extensions in the caller's own extension host, so a theme installed in another host (the UI side of a remote window, for instance) would be missed. The function therefore first tries the proposed allAcrossExtensionHosts API. It remembers in a module-level flag whether reading the property succeeded, and uses extensions.all when it threw. The comment above the function says the proposal is meant for internal builds. This repo's package.json has no enabledApiProposals, so a public build relies on the read throwing and takes the fallback. The test fake defines allAcrossExtensionHosts as a getter that returns all (source), so the tests always take the first branch.

The theme file is read with vscode.workspace.fs.readFile, decoded as UTF-8, and parsed. themeDir, the directory holding the file, is kept because every path inside a theme is relative to it (source).

3. Parsing the theme file

parseIconThemeJson is a single call to jsonc-parser's parse, cast to IconThemeDocument (source). JSONC matters because theme files, Seti's included, may contain comments and trailing commas, which JSON.parse rejects. Nothing is validated. A missing or mistyped field simply reads as undefined later. The types follow VS Code's file icon theme format, which the file links to.

IconThemeDocument and IconThemeVariant

icon_theme_json_parser.ts, lines 45–59

FieldTypeMeaningSource
filestring?Icon ID for files that match nothing else.L46
folder, folderExpandedstring?Folder icon IDs. Parsed, never read.L47–48
fileNamesRecord<string, string>?Exact (lower-case) file name → icon ID.L49
fileExtensionsRecord<string, string>?Extension without the dot → icon ID.L50
languageIdsRecord<string, string>?VS Code language ID → icon ID.L51
iconDefinitionsRecord<string, IconDefinition>?Icon ID → how to draw it.L52
fontsThemeFont[]?Document only. Fonts that font-glyph icons draw with.L56
lightIconThemeVariant?Document only. Overrides used under a light colour theme.L57
highContrastIconThemeVariant?Document only. Parsed, never read.L58

IconDefinition

icon_theme_json_parser.ts, lines 24–30

FieldTypeMeaningSource
iconPathstring?Image path, relative to the theme file. Takes priority over a glyph.L25
fontCharacterstring?Glyph code, for example \E029.L26
fontColorstring?Glyph colour. The service needs it for a font icon.L27
fontIdstring?Which ThemeFont to use. The first font if absent.L28
fontSizestring?Parsed, never read.L29

ThemeFont and ThemeFontSrc

icon_theme_json_parser.ts, lines 32–43

FieldTypeMeaningSource
idstring?The name fontId refers to. A font without one is skipped.L38
srcThemeFontSrc[]?Font files. Only the first entry's path is used.L39
weight, style, sizestring?Parsed, never read.L40–42
ThemeFontSrc.pathstringFont file, relative to the theme file.L33
ThemeFontSrc.formatstring?For example woff. Parsed, never read.L34

4. The IconTheme model

The private constructor flattens the document into lookup tables once (source). When activeColorTheme.kind is exactly Light, the light variant's three maps are spread over the base ones and its file replaces the base default. Otherwise the base is used as is. The default icon ID falls back to _default when neither sets file.

FieldTypeMeaningSource
themeIdstringThe ID it was loaded for. The cache key.L40
themeExtensionvscode.ExtensionThe extension that contributes the theme. Its URI is added to the webview's resource roots.L41
themeDirvscode.UriDirectory of the theme file, the base for icon and font paths.L43
fileNameToIconId, fileExtToIconId, languageIdToIconIdRecord<string, string>The theme's maps, with the light variant merged in.L28–30
defaultIconIdstringUsed when nothing matches.L33
fileNameToLanguageId, fileExtensionToLanguageIdRecord<string, string>Built from every installed extension's contributes.languages filenames and extensions, lower-cased, leading dot stripped. A later extension overwrites an earlier one.L149–166

Resolving a file name

getIconId lower-cases the name and stops at the first hit (source):

  1. fileNames[name].
  2. The language registered for that exact file name, then languageIds[language].
  3. fileExtensions[ext], where ext is the text after the last dot.
  4. The language registered for that extension, then languageIds[language].
  5. defaultIconId.

getIconDefinition then looks the ID up in the document's top-level iconDefinitions, and getFonts returns fonts or an empty list (source).

5. Serving icons to a webview

An IconThemeService is built for one vscode.Webview (source). Webviews can only load local files under their localResourceRoots, so when a theme loads, addExtensionUriToWebview appends the theme extension's URI to that list unless it is already there, by reassigning webview.options (source). Each font's first src path is then turned into a webview URI, keyed by font ID, and the first font's ID becomes the default (source).

getFileIcon(name) is synchronous and returns {} until a theme has been loaded (source). With a theme, an iconPath definition gives {iconUri}. A fontCharacter definition gives {font}, but only when a font family, its URI and a fontColor are all known. Anything else gives {}.

FileIcon

icon_theme_service.ts, lines 20–27

FieldTypeMeaningSource
iconUristring?Webview URI of an image icon.L23
fontFileFont?Set instead when the theme draws with a font. At most one of the two is set. Both are absent when nothing resolved.L26

FileFont

icon_theme_service.ts, lines 29–42

FieldTypeMeaningSource
fontUristringWebview URI of the font file.L32
fontCharacterstringThe glyph code.L35
fontColorstringThe glyph colour.L38
fontFamilystringThe font's ID, meant to be used as a CSS font family name.L41

Both interfaces' doc comments call them duplicates of FileIcon and FileFont in src/ui/commit_graph/api/types.ts, kept separate to avoid a dependency. Those types aren't there at this pin: the protocol's only file type is File, which has just a path.

6. Caching and invalidation

The cache is one slot per service: the IconTheme, its font URIs and the default font ID (source). loadTheme returns it unchanged when the configured theme ID matches the cached themeId. Otherwise it loads the new theme and replaces the slot (source). There is no event listener: the cache is only refreshed when someone calls loadTheme again. Because the key is only the theme ID, some changes are never picked up while the theme stays the same:

  • Switching between light and dark colour themes. The light variant is merged in the constructor.
  • Installing or removing an extension that contributes languages. The language maps are built in the constructor.
  • An update to the theme extension that changes its JSON.

7. Tests

Both tests install the fake VS Code API from src/testing/, register a FakeExtension contributing one icon theme, set workbench.iconTheme, write the theme JSON into the fake file system and load it into a service bound to a FakeWebview.

TestChecksSource
resolves image file iconsfile.ts resolves through fileExtensions to the TypeScript SVG. other.txt falls back to file. Both are vscode-webview:// URIs under the theme directory.25–81
resolves font-based file iconsfile.ts gets the glyph with its explicit fontId. other.txt gets a definition without fontId and so uses the first font. Both share the same font URI.83–163

Not covered: file-name and language-ID lookups, the light variant, the Seti fallback, label-only theme entries, and the resource-root update.

8. Loose ends at this pin

  • Not wired in. Nothing outside this directory imports IconThemeService. The Bazel target is visible to //src/ui/commit_graph_provider:__subpackages__, but that directory has no BUILD.bazel at this pin (source). The target also depends on //src/ui/commit_graph/api without importing it.
  • Extensions with dots. Only the text after the last dot is tried, so a theme's d.ts or test.ts entries never match. VS Code itself tries the longer extensions first.
  • Names with a path or no dot. The whole argument is looked up as a file name, so a path like src/a.ts never hits fileNames. A name with no dot is also tried as an extension.
  • Variants. highContrast is ignored, and so is light under the high-contrast light kind. A variant's own iconDefinitions are never consulted.
  • Folders and font details. Folder icons, fontSize, font weight, style, format and every src after the first are dropped. A font glyph without fontColor resolves to nothing, though the format lets it inherit the text colour.
  • The Seti fallback path is hard-coded, so it would break if VS Code moved that file.