Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[icon-themes] Support glob keys in file icon associations #174286

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8568c66
Support globs in file icon themes (#12493)
zm-cttae Feb 13, 2023
e0e9123
Fix CI compile error
zm-cttae Feb 13, 2023
faccd74
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Feb 13, 2023
e487bd1
Fix post-glob ext file icon classes (#174286)
zm-cttae Feb 14, 2023
b88fead
Fix coaelesced glob regex
zm-cttae Feb 14, 2023
fe76b5f
Narrow folder name type in getIconClasses
zm-cttae Feb 14, 2023
c022fc6
Polishing
zm-cttae Feb 14, 2023
f4e9f33
Update CSS glob comments post-polish
zm-cttae Feb 14, 2023
9eb51e0
Typo: file -> folder
zm-cttae Feb 14, 2023
eda562c
Polish up variable names
zm-cttae Feb 14, 2023
b83b446
let -> const
zm-cttae Feb 14, 2023
3e8e0cf
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Feb 14, 2023
24c081f
Reduce glob icon priority of using name score classes
zm-cttae Feb 14, 2023
e34c92d
Cleanup pass on CSS glob comments
zm-cttae Feb 14, 2023
7d0f02a
Fix regression (folder-specifc icons mixing with files)
zm-cttae Feb 14, 2023
35cded5
Remove switch for dash separator
zm-cttae Feb 14, 2023
277940e
let -> const
zm-cttae Feb 14, 2023
34a982f
Cleanup pass
zm-cttae Feb 16, 2023
917618b
Apply "1 file extension" restriction to dash/underscore globs
zm-cttae Feb 17, 2023
d0ec167
[nit] use icon kind in dirname per #136656
zm-cttae Feb 25, 2023
5c5ec59
Use `*` (non-coalescing wildcard) to match multiple dots
zm-cttae Mar 9, 2023
6ebfb66
Remove permutative and wildcard chain globs
zm-cttae Mar 10, 2023
d06ed11
Ban icon globs for >=5 filename dot segments
zm-cttae Mar 10, 2023
95b759f
Note limit in filename extension comments
zm-cttae Mar 10, 2023
64be550
Limit icon glob wildcard to file extensions (>=2nd segment)
zm-cttae Mar 13, 2023
5875d0a
Fix dash separated segment globs
zm-cttae Mar 13, 2023
6612b61
[nit] Move basenameGlob comment above #segments!=2 if-return
zm-cttae Mar 13, 2023
390248a
[nit] Test typeof filename is string for icon classes
zm-cttae Mar 13, 2023
f61680f
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Mar 15, 2023
6256740
[nit] Rename base-glob vars to wildcard-glob
zm-cttae Mar 15, 2023
bbf1340
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Mar 17, 2023
945ea3a
Merge pull request #1 from zm-cttae/pr-174286/hotfix-issuecomment-146…
zm-cttae Mar 20, 2023
cd4c88b
Merge branch 'microsoft:main' into feat-file-icon-theme-glob
zm-cttae Oct 2, 2023
0e877f1
WIP - implement simpler file icon glob assocations (#177650)
zm-cttae Nov 8, 2024
9c4c754
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Nov 8, 2024
1cf48d3
Fix missing import
zm-cttae Nov 8, 2024
b5e651d
Fix dirname refactor
zm-cttae Nov 9, 2024
0d8f64e
Wire up icon attributes for file icon globs
zm-cttae Nov 9, 2024
d7c3813
Fix readonly type error in icon label options
zm-cttae Nov 10, 2024
6cf396e
Wire up chat widget labels
zm-cttae Nov 10, 2024
5562af8
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Nov 10, 2024
eff148e
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Nov 15, 2024
bb0c7f2
Fix pushGlobSelectors compile error
zm-cttae Nov 15, 2024
553abf7
Escape handleParentFolder CSS string
zm-cttae Nov 15, 2024
a2ef413
Get CI passing
zm-cttae Nov 16, 2024
8e9f705
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Nov 16, 2024
1f8f44e
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Dec 29, 2024
d130c6f
[nit] Fix indent in chatInlineAnchorWidget.ts
zm-cttae Dec 29, 2024
a55a9ac
Merge branch 'main' into feat-file-icon-theme-glob
zm-cttae Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/vs/base/browser/ui/iconLabel/iconLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface IIconLabelValueOptions {
suffix?: string;
hideIcon?: boolean;
extraClasses?: readonly string[];
extraAttributes?: { [attrName: string]: string };
italic?: boolean;
strikethrough?: boolean;
matches?: readonly IMatch[];
Expand Down Expand Up @@ -132,6 +133,7 @@ export class IconLabel extends Disposable {
setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {
const labelClasses = ['monaco-icon-label'];
const containerClasses = ['monaco-icon-label-container'];
const dataAttributes = {};
let ariaLabel: string = '';
if (options) {
if (options.extraClasses) {
Expand All @@ -149,6 +151,11 @@ export class IconLabel extends Disposable {
if (options.disabledCommand) {
containerClasses.push('disabled');
}

if (options.extraAttributes) {
Object.assign(dataAttributes, options.extraAttributes);
}

if (options.title) {
if (typeof options.title === 'string') {
ariaLabel += options.title;
Expand All @@ -174,6 +181,7 @@ export class IconLabel extends Disposable {

this.domNode.classNames = labelClasses;
this.domNode.element.setAttribute('aria-label', ariaLabel);
Object.assign(this.domNode.element.dataset, dataAttributes);
this.labelContainer.classList.value = '';
this.labelContainer.classList.add(...containerClasses);
this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);
Expand Down
91 changes: 63 additions & 28 deletions src/vs/editor/common/services/getIconClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ILanguageService } from '../languages/language.js';
import { IModelService } from './model.js';
import { FileKind } from '../../../platform/files/common/files.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { extname, basename } from '../../../base/common/path.js';

const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/;

Expand All @@ -24,51 +25,44 @@ export function getIconClasses(modelService: IModelService, languageService: ILa
}

// we always set these base classes even if we do not have a path
const classes = fileKind === FileKind.ROOT_FOLDER ? ['rootfolder-icon'] : fileKind === FileKind.FOLDER ? ['folder-icon'] : ['file-icon'];
const kindClass = fileKind === FileKind.ROOT_FOLDER ? 'rootfolder-icon' : fileKind === FileKind.FOLDER ? 'folder-icon' : 'file-icon';
const classes = [kindClass];
if (resource) {
const { filename, dirname } = getResourceName(resource);

// Get the path and name of the resource. For data-URIs, we need to parse specially
let name: string | undefined;
if (resource.scheme === Schemas.data) {
const metadata = DataUri.parseMetaData(resource);
name = metadata.get(DataUri.META_DATA_LABEL);
} else {
const match = resource.path.match(fileIconDirectoryRegex);
if (match) {
name = fileIconSelectorEscape(match[2].toLowerCase());
if (match[1]) {
classes.push(`${fileIconSelectorEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory
}
if (dirname) {
classes.push(`${dirname}-dirname-${kindClass}`); // parent directory
}

} else {
name = fileIconSelectorEscape(resource.authority.toLowerCase());
}
// Get dot segments for filename, and avoid doing an explosive combination of segments
// (from a filename with lots of `.` characters; most file systems do not allow files > 255 length)
// https://github.com/microsoft/vscode/issues/116199
let segments: string[] | undefined;
if (typeof filename === 'string' && filename.length <= 255) {
segments = filename.replace(/\.\.\.+/g, '').split('.');
}

// Root Folders
if (fileKind === FileKind.ROOT_FOLDER) {
classes.push(`${name}-root-name-folder-icon`);
classes.push(`${filename}-root-name-folder-icon`);
}

// Folders
else if (fileKind === FileKind.FOLDER) {
classes.push(`${name}-name-folder-icon`);
if (typeof filename === 'string' && fileKind === FileKind.FOLDER) {
classes.push(`${filename}-name-folder-icon`);
classes.push(`name-folder-icon`); // extra segment to increase folder-name score
}

// Files
else {

// Name & Extension(s)
if (name) {
classes.push(`${name}-name-file-icon`);
if (typeof filename === 'string') {
classes.push(`${filename}-name-file-icon`);
classes.push(`name-file-icon`); // extra segment to increase file-name score
// Avoid doing an explosive combination of extensions for very long filenames
// (most file systems do not allow files > 255 length) with lots of `.` characters
// https://github.com/microsoft/vscode/issues/116199
if (name.length <= 255) {
const dotSegments = name.split('.');
for (let i = 1; i < dotSegments.length; i++) {
classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one
if (filename.length <= 255 && segments) {
for (let i = 1; i < segments.length; i++) {
classes.push(`${segments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one
}
}
classes.push(`ext-file-icon`); // extra segment to increase file-ext score
Expand All @@ -88,6 +82,23 @@ export function getIconClassesForLanguageId(languageId: string): string[] {
return ['file-icon', `${fileIconSelectorEscape(languageId)}-lang-file-icon`];
}

export function getIconAttributes(resource: uri | undefined) {
const attributes: Record<string, string> = {};

if (resource) {
const { filename } = getResourceName(resource);

if (filename) {
const fileExtname = extname(filename);
const fileBasename = basename(filename, fileExtname);
attributes.fileIconExtname = fileExtname.substring(1);
attributes.fileIconBasename = fileBasename;
}
}

return attributes;
}

function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null {
if (!resource) {
return null; // we need a resource at least
Expand Down Expand Up @@ -122,6 +133,30 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag
return languageService.guessLanguageIdByFilepathOrFirstLine(resource);
}

function getResourceName(resource: uri) {
// Get the path and name of the resource. For data-URIs, we need to parse specially
let filename: string | undefined;
let dirname: string | undefined;

if (resource.scheme === Schemas.data) {
const metadata = DataUri.parseMetaData(resource);
filename = metadata.get(DataUri.META_DATA_LABEL);

} else {
const match = resource.path.match(fileIconDirectoryRegex);
if (match) {
filename = fileIconSelectorEscape(match[2].toLowerCase());
if (match[1]) {
dirname = fileIconSelectorEscape(match[1].toLowerCase());
}
} else {
filename = fileIconSelectorEscape(resource.authority.toLowerCase());
}
}

return { filename, dirname };
}

export function fileIconSelectorEscape(str: string): string {
return str.replace(/[\s]/g, '/'); // HTML class names can not contain certain whitespace characters (https://dom.spec.whatwg.org/#interface-domtokenlist), use / instead, which doesn't exist in file names.
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../browser/editorBrowser.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
import { CompletionItemKind, CompletionItemKinds, CompletionItemTag } from '../../../common/languages.js';
import { getIconClasses } from '../../../common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../../common/services/getIconClasses.js';
import { IModelService } from '../../../common/services/model.js';
import { ILanguageService } from '../../../common/languages/language.js';
import * as nls from '../../../../nls.js';
Expand Down Expand Up @@ -184,6 +184,9 @@ export class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTe
const labelClasses = getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: element.textLabel }), FileKind.FILE);
const detailClasses = getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: completion.detail }), FileKind.FILE);
labelOptions.extraClasses = labelClasses.length > detailClasses.length ? labelClasses : detailClasses;
const labelAttributes = getIconAttributes(URI.from({ scheme: 'fake', path: element.textLabel }));
const detailAttributes = getIconAttributes(URI.from({ scheme: 'fake', path: completion.detail }));
labelOptions.extraAttributes = Object.assign(detailAttributes, labelAttributes);

} else if (completion.kind === CompletionItemKind.Folder && this._themeService.getFileIconTheme().hasFolderIcons) {
// special logic for 'folder' completion items
Expand All @@ -193,6 +196,10 @@ export class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTe
getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: element.textLabel }), FileKind.FOLDER),
getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: completion.detail }), FileKind.FOLDER)
].flat();
labelOptions.extraAttributes = Object.assign(
getIconAttributes(URI.from({ scheme: 'fake', path: element.textLabel })),
getIconAttributes(URI.from({ scheme: 'fake', path: completion.detail }))
);
} else {
// normal icon
data.icon.className = 'icon hide';
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/quickinput/common/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface IQuickPickItem {
*/
keybinding?: ResolvedKeybinding;
iconClasses?: readonly string[];
iconAttributes?: Record<string, string>;
iconPath?: { dark: URI; light?: URI };
iconClass?: string;
italic?: boolean;
Expand Down
7 changes: 6 additions & 1 deletion src/vs/workbench/browser/actions/windowActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { IModelService } from '../../../editor/common/services/model.js';
import { ILanguageService } from '../../../editor/common/languages/language.js';
import { IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService } from '../../../platform/workspaces/common/workspaces.js';
import { URI } from '../../../base/common/uri.js';
import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../../editor/common/services/getIconClasses.js';
import { FileKind } from '../../../platform/files/common/files.js';
import { splitRecentLabel } from '../../../base/common/labels.js';
import { isMacintosh, isWeb, isWindows } from '../../../base/common/platform.js';
Expand Down Expand Up @@ -185,6 +185,7 @@ abstract class BaseOpenRecentAction extends Action2 {
private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick {
let openable: IWindowOpenable | undefined;
let iconClasses: string[];
let iconAttributes: Record<string, string>;
let fullLabel: string | undefined;
let resource: URI | undefined;
let isWorkspace = false;
Expand All @@ -193,6 +194,7 @@ abstract class BaseOpenRecentAction extends Action2 {
if (isRecentFolder(recent)) {
resource = recent.folderUri;
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FOLDER);
iconAttributes = getIconAttributes(resource);
openable = { folderUri: resource };
fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: Verbosity.LONG });
}
Expand All @@ -201,6 +203,7 @@ abstract class BaseOpenRecentAction extends Action2 {
else if (isRecentWorkspace(recent)) {
resource = recent.workspace.configPath;
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.ROOT_FOLDER);
iconAttributes = getIconAttributes(resource);
openable = { workspaceUri: resource };
fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: Verbosity.LONG });
isWorkspace = true;
Expand All @@ -210,6 +213,7 @@ abstract class BaseOpenRecentAction extends Action2 {
else {
resource = recent.fileUri;
iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FILE);
iconAttributes = getIconAttributes(resource);
openable = { fileUri: resource };
fullLabel = recent.label || labelService.getUriLabel(resource, { appendWorkspaceSuffix: true });
}
Expand All @@ -218,6 +222,7 @@ abstract class BaseOpenRecentAction extends Action2 {

return {
iconClasses,
iconAttributes,
label: name,
ariaLabel: isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,
description: parentPath,
Expand Down
5 changes: 3 additions & 2 deletions src/vs/workbench/browser/actions/workspaceCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { FileKind } from '../../../platform/files/common/files.js';
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../platform/label/common/label.js';
import { IQuickInputService, IPickOptions, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';
import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../../editor/common/services/getIconClasses.js';
import { IModelService } from '../../../editor/common/services/model.js';
import { ILanguageService } from '../../../editor/common/languages/language.js';
import { IFileDialogService, IPickAndOpenOptions } from '../../../platform/dialogs/common/dialogs.js';
Expand Down Expand Up @@ -124,7 +124,8 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, async functio
label,
description: description !== label ? description : undefined, // https://github.com/microsoft/vscode/issues/183418
folder,
iconClasses: getIconClasses(modelService, languageService, folder.uri, FileKind.ROOT_FOLDER)
iconClasses: getIconClasses(modelService, languageService, folder.uri, FileKind.ROOT_FOLDER),
iconAttributes: getIconAttributes(folder.uri)
};
});

Expand Down
15 changes: 13 additions & 2 deletions src/vs/workbench/browser/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ITextModel } from '../../editor/common/model.js';
import { IThemeService } from '../../platform/theme/common/themeService.js';
import { Event, Emitter } from '../../base/common/event.js';
import { ILabelService } from '../../platform/label/common/label.js';
import { getIconClasses } from '../../editor/common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../editor/common/services/getIconClasses.js';
import { Disposable, dispose, IDisposable, MutableDisposable } from '../../base/common/lifecycle.js';
import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';
import { normalizeDriveLetter } from '../../base/common/labels.js';
Expand Down Expand Up @@ -295,6 +295,7 @@ class ResourceLabelWidget extends IconLabel {
private options: IResourceLabelOptions | undefined = undefined;

private computedIconClasses: string[] | undefined = undefined;
private computedIconAttributes: Record<string, string> | undefined = undefined;
private computedLanguageId: string | undefined = undefined;
private computedPathLabel: string | undefined = undefined;
private computedWorkspaceFolderLabel: string | undefined = undefined;
Expand Down Expand Up @@ -570,13 +571,14 @@ class ResourceLabelWidget extends IconLabel {
return false;
}

const iconLabelOptions: IIconLabelValueOptions & { extraClasses: string[] } = {
const iconLabelOptions: IIconLabelValueOptions & { extraClasses: string[]; extraAttributes: { [attrName: string]: string } } = {
title: '',
italic: this.options?.italic,
strikethrough: this.options?.strikethrough,
matches: this.options?.matches,
descriptionMatches: this.options?.descriptionMatches,
extraClasses: [],
extraAttributes: {},
separator: this.options?.separator,
domId: this.options?.domId,
disabledCommand: this.options?.disabledCommand,
Expand Down Expand Up @@ -612,17 +614,26 @@ class ResourceLabelWidget extends IconLabel {
this.computedIconClasses = getIconClasses(this.modelService, this.languageService, resource, this.options.fileKind, this.options.icon);
}

if (!this.computedIconAttributes) {
this.computedIconAttributes = getIconAttributes(resource);
}

if (URI.isUri(this.options.icon)) {
iconLabelOptions.iconPath = this.options.icon;
}

iconLabelOptions.extraClasses = this.computedIconClasses.slice(0);
iconLabelOptions.extraAttributes = this.computedIconAttributes;
}

if (this.options?.extraClasses) {
iconLabelOptions.extraClasses.push(...this.options.extraClasses);
}

if (this.options?.extraAttributes) {
Object.assign(iconLabelOptions.extraAttributes, this.options.extraAttributes);
}

if (this.options?.fileDecorations && resource) {
if (options.updateDecoration) {
this.decoration.value = this.decorationsService.getDecoration(resource, this.options.fileKind !== FileKind.FILE);
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/browser/parts/editor/editorQuickAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { EditorsOrder, IEditorIdentifier, EditorResourceAccessor, SideBySideEdit
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
import { prepareQuery, scoreItemFuzzy, compareItemsByFuzzyScore, FuzzyScorerCache } from '../../../../base/common/fuzzyScorer.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
Expand Down Expand Up @@ -158,6 +158,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
})(),
description,
iconClasses: getIconClasses(this.modelService, this.languageService, resource, undefined, editor.getIcon()).concat(editor.getLabelExtraClasses()),
iconAttributes: getIconAttributes(resource),
italic: !this.editorGroupService.getGroup(groupId)?.isPinned(editor),
buttons: (() => {
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownR
import { Range } from '../../../../../editor/common/core/range.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';
import { getIconAttributes, getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { localize } from '../../../../../nls.js';
Expand Down Expand Up @@ -329,16 +329,21 @@ class CollapsedCodeBlock extends Disposable {
const isComplete = !modifiedEntry?.isCurrentlyBeingModified.get();

let iconClasses: string[] = [];
let iconAttributes: Record<string, string> = {};
if (isStreaming || !isComplete) {
const codicon = ThemeIcon.modify(Codicon.loading, 'spin');
iconClasses = ThemeIcon.asClassNameArray(codicon);
iconAttributes = getIconAttributes(undefined);
} else {
const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
iconClasses = getIconClasses(this.modelService, this.languageService, uri, fileKind);
iconAttributes = getIconAttributes(uri);
}

const iconEl = dom.$('span.icon');
iconEl.classList.add(...iconClasses);
Object.assign(iconEl.dataset, iconAttributes);
this.element.replaceChildren(iconEl, dom.$('span.icon-label', {}, iconText));

const children = [dom.$('span.icon-label', {}, iconText)];
if (isStreaming) {
Expand Down
Loading