diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 981933151ccbb..8761b9ae95187 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -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[]; @@ -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) { @@ -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; @@ -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); diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index cb60d8b7bf52c..4cf259c604526 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -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 = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; @@ -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 @@ -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 = {}; + + 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 @@ -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. } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts index 3d0850da13b19..243808123fa87 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts @@ -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'; @@ -184,6 +184,9 @@ export class ItemRenderer implements IListRenderer 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 @@ -193,6 +196,10 @@ export class ItemRenderer implements IListRenderer; iconPath?: { dark: URI; light?: URI }; iconClass?: string; italic?: boolean; diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 2fb9c8edbb3d5..f8b761e47e0b6 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -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'; @@ -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; let fullLabel: string | undefined; let resource: URI | undefined; let isWorkspace = false; @@ -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 }); } @@ -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; @@ -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 }); } @@ -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, diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 5d45d2f2cfa3b..f4a987a899d1c 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -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'; @@ -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) }; }); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index f83bdc050b8af..8f59f216263ff 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -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'; @@ -295,6 +295,7 @@ class ResourceLabelWidget extends IconLabel { private options: IResourceLabelOptions | undefined = undefined; private computedIconClasses: string[] | undefined = undefined; + private computedIconAttributes: Record | undefined = undefined; private computedLanguageId: string | undefined = undefined; private computedPathLabel: string | undefined = undefined; private computedWorkspaceFolderLabel: string | undefined = undefined; @@ -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, @@ -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); diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index bbab2a62d6e2b..f2cfe80eb2d1a 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -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'; @@ -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 [ diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index a281df1c40cee..9650b51f8ce19 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -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'; @@ -329,16 +329,21 @@ class CollapsedCodeBlock extends Disposable { const isComplete = !modifiedEntry?.isCurrentlyBeingModified.get(); let iconClasses: string[] = []; + let iconAttributes: Record = {}; 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) { diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index e74cab4ab0c33..5be6415725d6a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -16,7 +16,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { Location, SymbolKinds } from '../../../../editor/common/languages.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 { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; @@ -100,6 +100,7 @@ export class InlineAnchorWidget extends Disposable { let iconText: string; let iconClasses: string[]; + let iconAttributes: Record; let location: { readonly uri: URI; readonly range?: IRange }; @@ -109,7 +110,9 @@ export class InlineAnchorWidget extends Disposable { location = this.data.symbol.location; iconText = this.data.symbol.name; + iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))]; + iconAttributes = getIconAttributes(undefined); this._register(dom.addDisposableListener(element, 'click', () => { telemetryService.publicLog2<{ @@ -134,6 +137,7 @@ export class InlineAnchorWidget extends Disposable { const fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; iconClasses = getIconClasses(modelService, languageService, location.uri, fileKind); + iconAttributes = getIconAttributes(location.uri); const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService); fileService.stat(location.uri) @@ -186,6 +190,7 @@ export class InlineAnchorWidget extends Disposable { const iconEl = dom.$('span.icon'); iconEl.classList.add(...iconClasses); + Object.assign(iconEl.dataset, iconAttributes); element.replaceChildren(iconEl, dom.$('span.icon-label', {}, iconText)); const fragment = location.range ? `${location.range.startLineNumber},${location.range.startColumn}` : ''; diff --git a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts index 680ba4579abb0..596083bb45e69 100644 --- a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts +++ b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts @@ -8,7 +8,7 @@ import { Source } from './debugSource.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { IDebugService, IDebugSession } from './debug.js'; import { IEditorService } from '../../../services/editor/common/editorService.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 { DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -97,6 +97,7 @@ function _createPick(source: Source, filter: string, editorService: IEditorServi description: desc === '.' ? undefined : desc, highlights: { label: labelHighlights ?? undefined, description: descHighlights ?? undefined }, iconClasses: getIconClasses(modelService, languageService, source.uri), + iconAttributes: getIconAttributes(source.uri), accept: () => { if (source.available) { source.openInEditor(editorService, { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 }); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index 2666528ab8286..06a7f8f7aaa81 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -24,7 +24,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ActiveEditorContext, ResourceContextKey } from '../../../common/contextkeys.js'; import { IQuickInputService, 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 { ILabelService } from '../../../../platform/label/common/label.js'; @@ -372,7 +372,8 @@ registerAction2(class extends Action2 { resource, label: basenameOrAuthority(resource), description: labelService.getUriLabel(dirname(resource), { relative: true }), - iconClasses: getIconClasses(modelService, languageService, resource) + iconClasses: getIconClasses(modelService, languageService, resource), + iconAttributes: getIconAttributes(resource) })); await Event.toPromise(resourcePicker.onDidAccept); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 1dc3bb5b7d7ce..d8b987e2622af 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -13,7 +13,7 @@ import { EditorContextKeys } from '../../../../../editor/common/editorContextKey import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ILanguageConfigurationService } from '../../../../../editor/common/languages/languageConfigurationRegistry.js'; import { TrackedRangeStickiness } 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 { LineCommentCommand, Type } from '../../../../../editor/contrib/comment/browser/lineCommentCommand.js'; import { localize, localize2 } from '../../../../../nls.js'; @@ -456,6 +456,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction getIconClasses(this.modelService, this.languageService, resource, undefined, icon).concat(extraClasses)); + const iconAttributesValue = new Lazy(() => getIconAttributes(resource)); + const buttonsValue = new Lazy(() => { const openSideBySideDirection = configuration.openSideBySideDirection; const buttons: IQuickInputButton[] = []; @@ -1015,6 +1017,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { switch (buttonIndex) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 9218eff9f83b0..642f2c603c660 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -50,7 +50,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { dirname } from '../../../../base/common/resources.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 { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { killTerminalIcon, newTerminalIcon } from './terminalIcons.js'; @@ -1659,7 +1659,8 @@ async function pickTerminalCwd(accessor: ServicesAccessor, cancel?: Cancellation label, description: description !== label ? description : undefined, pair: pair, - iconClasses: getIconClasses(modelService, languageService, pair.cwd, FileKind.ROOT_FOLDER) + iconClasses: getIconClasses(modelService, languageService, pair.cwd, FileKind.ROOT_FOLDER), + iconAttributes: getIconAttributes(pair.cwd) }; }); const options: IPickOptions = { diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index ce2a584233d06..94ad6085e1b03 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -13,7 +13,7 @@ import { FileKind } from '../../../platform/files/common/files.js'; import { IModelService } from '../../../editor/common/services/model.js'; import { ILanguageService } from '../../../editor/common/languages/language.js'; import { IQuickInputService, IQuickInputButton, IQuickPickItem, QuickPickInput } from '../../../platform/quickinput/common/quickInput.js'; -import { getIconClasses } from '../../../editor/common/services/getIconClasses.js'; +import { getIconAttributes, getIconClasses } from '../../../editor/common/services/getIconClasses.js'; import { ICommandHandler } from '../../../platform/commands/common/commands.js'; import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; @@ -273,6 +273,7 @@ abstract class BaseSwitchWindow extends Action2 { label: window.title, ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title, iconClasses: getIconClasses(modelService, languageService, resource, fileKind), + iconAttributes: getIconAttributes(resource), description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined, buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined }; @@ -280,10 +281,12 @@ abstract class BaseSwitchWindow extends Action2 { if (auxiliaryWindows) { for (const auxiliaryWindow of auxiliaryWindows) { + const resource = auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined; const pick: IWindowPickItem = { windowId: auxiliaryWindow.id, label: auxiliaryWindow.title, - iconClasses: getIconClasses(modelService, languageService, auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined, FileKind.FILE), + iconClasses: getIconClasses(modelService, languageService, resource, FileKind.FILE), + iconAttributes: getIconAttributes(resource), description: (currentWindowId === auxiliaryWindow.id) ? localize('current', "Current Window") : undefined, buttons: [this.closeWindowAction] }; diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index af5292d79826d..c53b138481c49 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -16,7 +16,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { INotificationService } from '../../../../platform/notification/common/notification.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 { Schemas } from '../../../../base/common/network.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; @@ -1038,9 +1038,21 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { if (stat.isDirectory) { const filename = resources.basename(fullPath); fullPath = resources.addTrailingPathSeparator(fullPath, this.separator); - return { label: filename, uri: fullPath, isFolder: true, iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined, FileKind.FOLDER) }; + return { + label: filename, + uri: fullPath, + isFolder: true, + iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined, FileKind.FOLDER), + iconAttributes: getIconAttributes(fullPath || undefined), + }; } else if (!stat.isDirectory && this.allowFileSelection && this.filterFile(fullPath)) { - return { label: stat.name, uri: fullPath, isFolder: false, iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined) }; + return { + label: stat.name, + uri: fullPath, + isFolder: false, + iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined), + iconAttributes: getIconAttributes(fullPath || undefined), + }; } return undefined; } diff --git a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts index fcaf2fd9c410b..4536db46a84a6 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts @@ -7,7 +7,7 @@ import { distinct } from '../../../../base/common/arrays.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { JSONPath, parse } from '../../../../base/common/json.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; +import { getIconClasses, getIconAttributes } from '../../../../editor/common/services/getIconClasses.js'; import { FileKind, IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -259,6 +259,7 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor label: workspaceFolder.name, description: localize('workspace folder', "Workspace Folder"), workspaceOrFolder: workspaceFolder, + iconAttributes: getIconAttributes(workspaceFolder.uri), iconClasses: getIconClasses(this.modelService, this.languageService, workspaceFolder.uri, FileKind.ROOT_FOLDER) }; }); diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index a6e4c965f4957..f8d7aa7e9f4c3 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -296,18 +296,34 @@ export class FileIconThemeLoader { if (folderNames) { for (const key in folderNames) { const selectors = new css.Builder(); - const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); + + if (!name.includes('*')) { + selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + selectors.push(css.inline`.name-folder-icon`); // extra segment to increase folder-name score + } + + pushGlobSelectors(name, selectors, 'folder'); + addSelector(css.inline`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); result.hasFolderIcons = true; } } + const folderNamesExpanded = associations.folderNamesExpanded; if (folderNamesExpanded) { for (const key in folderNamesExpanded) { const selectors = new css.Builder(); - const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); + + if (!name.includes('*')) { + selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + selectors.push(css.inline`.folder-icon`); // extra segment to increase folder-name score + selectors.push(css.inline`.name-folder-icon`); // extra segment to increase folder-name score + } + + pushGlobSelectors(name, selectors, 'folder'); + addSelector(css.inline`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } @@ -346,7 +362,8 @@ export class FileIconThemeLoader { if (fileExtensions) { for (const key in fileExtensions) { const selectors = new css.Builder(); - const name = handleParentFolder(key.toLowerCase(), selectors); + const name = handleParentFolder(key.toLowerCase(), selectors, 'file'); + const segments = name.split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { @@ -354,6 +371,7 @@ export class FileIconThemeLoader { } selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } + addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]); result.hasFileIcons = true; hasSpecificFileIcons = true; @@ -363,9 +381,15 @@ export class FileIconThemeLoader { if (fileNames) { for (const key in fileNames) { const selectors = new css.Builder(); - const fileName = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(css.inline`.${classSelectorPart(fileName)}-name-file-icon`); - selectors.push(css.inline`.name-file-icon`); // extra segment to increase file-name score + const fileName = handleParentFolder(key.toLowerCase(), selectors, 'file'); + + if (!fileName.includes('*')) { + selectors.push(css.inline`.${classSelectorPart(fileName)}-name-file-icon`); + selectors.push(css.inline`.name-file-icon`); // extra segment to increase file-name score + } + + pushGlobSelectors(fileName, selectors, 'file'); + const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { @@ -373,6 +397,7 @@ export class FileIconThemeLoader { } selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } + addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]); result.hasFileIcons = true; hasSpecificFileIcons = true; @@ -482,11 +507,38 @@ export class FileIconThemeLoader { } } -function handleParentFolder(key: string, selectors: css.Builder): string { +function pushGlobSelectors(name: string, selectors: css.Builder, kind: string): void { + const extname = paths.extname(name); + const basename = paths.basename(name, extname); + selectors.push(css.inline`${classSelectorPart(getGlobSelector(basename, kind, 'basename'))}`); + selectors.push(css.inline`${classSelectorPart(getGlobSelector(extname.substring(1), kind, 'extname'))}`); +} + +function getGlobSelector(key: string, kind: string, portion: string): string { + if (key === '*') { + return `[data-${kind}-icon-${portion}]`; + } + if (key.startsWith('*') && key.endsWith('*')) { + return `[data-${kind}-icon-${portion}*="${key.slice(1, -1)}"]`; + } + if (key.startsWith('*')) { + return `[data-${kind}-icon-${portion}$="${key.slice(1)}"]`; + } + if (key.endsWith('*')) { + return `[data-${kind}-icon-${portion}^="${key.slice(0, -1)}"]`; + } + if (key.indexOf('*') !== -1 && key.indexOf('*') === key.lastIndexOf('*')) { + const [prefix, suffix] = key.split('*'); + return `[data-${kind}-icon-${portion}^="${prefix}"][data-${kind}-icon-${portion}$="${suffix}"]`; + } + return `[data-${kind}-icon-${portion}="${key}"]`; +} + +function handleParentFolder(key: string, selectors: css.Builder, kind: string): string { const lastIndexOfSlash = key.lastIndexOf('/'); if (lastIndexOfSlash >= 0) { const parentFolder = key.substring(0, lastIndexOfSlash); - selectors.push(css.inline`.${classSelectorPart(parentFolder)}-name-dir-icon`); + selectors.push(css.inline`.${classSelectorPart(parentFolder)}-dirname-${classSelectorPart(kind)}-icon`); return key.substring(lastIndexOfSlash + 1); } return key;