From 8568c6690a3cc41312a5af934ee41df233691832 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:06:05 +0000 Subject: [PATCH 01/37] Support globs in file icon themes (#12493) --- .../editor/common/services/getIconClasses.ts | 90 ++++++++++++++++++- .../themes/browser/fileIconThemeData.ts | 15 ++-- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 464555c63fbf7..0cc791fde6ffa 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -12,6 +12,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { FileKind } from 'vs/platform/files/common/files'; const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; +const fileIconCoalescedGlobRegex = /(?<=\*)(?:\.\*)+/; export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind): string[] { @@ -40,6 +41,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Folders if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); + pushGlobIconClasses(classes, name); } // Files @@ -53,12 +55,13 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // (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 + const segments = name.split('.'); + 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 + pushGlobIconClasses(classes, name); } // Detected Mode @@ -75,6 +78,87 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } +// Generate globs matching file icon themes +function pushGlobIconClasses(classes: string[], name: string, kind: string) { + // Remove ellipsis to defend against explosive combination + const segments = name.replace(/\.\.\.+/g, '').split('.'); + + // Limit permutative ("full") glob generation to <=4 file extensions. + if (segments.length < 5) { + const bitmask = Math.pow(2, segments.length) - 1; + + // All globs excluding those with chained `*` dot segments + for (let i = 0; i < bitmask; i++) { + let buffer = []; + for (let j = 0; j < segments.length; j++) { + const base = Math.pow(2, j); + buffer.push(i & base ? segments[j] : '*'); + } + const glob = buffer.join('.'); + + // Globs with chained * filename segments coaelesced into ** + if (glob.match(fileIconCoalescedGlobRegex)) { + const coaelescedGlob = glob.replace(fileIconCoalescedGlobRegex, '**'); + classes.push(`${coaelescedGlob}-glob-${kind}-icon`); + } + + // Globs including chained * dot segments + classes.push(`${glob}-glob-${kind}-icon`); + } + } else { + const lastDotIndex = segments.length - 1; + + for (let i = 0; i < lastDotIndex; i++) { + // Prefix-matching coalescing globs + const suffixSegments = segments.slice(0, i + 1); + suffixSegments.push(i < lastDotIndex - 1 ? '**' : '*'); + const suffixGlob = suffixSegments.join('.'); + classes.push(`${suffixGlob}-glob-${kind}-icon`); + + // Non-coalescing wildcard globs + const baseSegments = segments.slice(); + baseSegments[i] = '*'; + const baseGlob = baseSegments.join('.'); + classes.push(`${baseGlob}-glob-${kind}-icon`); + } + } + + // Simplest-case globs for dashed file basenames. + // Targets tooling filename conventions. + const dotLastIndex = name.lastIndexOf('.'); + if ( + dotLastIndex !== -1 && // >=1 file extensions + dotLastIndex === name.indexOf('.') // <=1 file extension + ) { + const extname = name.substring(dotLastIndex); + const basename = name.substring(name.lastIndexOf('.', dotLastIndex - 1), dotLastIndex); + + let separator = basename.match(/_|-/)?.[0]; + switch (true) { + case basename.indexOf('_') > -1: + separator = '_'; + break; + case basename.indexOf('-') > -1: + separator = '-'; + break; + } + + if (separator) { + // Prefix basename glob e.g. `test_*.py` + const basenameDashIndex = basename.indexOf(separator); + const basenamePrefix = basename.substring(0, basenameDashIndex); + const basenamePrefixGlob = basenamePrefix + separator + '*' + extname; + classes.push(`${basenamePrefixGlob}-glob-${kind}-icon`); + + // Suffix basename glob e.g. `*_test.go` + const basenameLastDashIndex = basename.lastIndexOf(separator); + const basenameSuffix = basename.substring(basenameLastDashIndex + 1); + const basenameSuffixGlob = '*' + separator + basenameSuffix + extname; + classes.push(`${basenameSuffixGlob}-glob-${kind}-icon`); + } + } +} + function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null { if (!resource) { return null; // we need a resource at least diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index b65836c237747..be321ddbe7002 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -293,7 +293,8 @@ export class FileIconThemeLoader { for (const key in folderNames) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(name)}-name-folder-icon`); + const kind = name.indexOf('*') !== -1 ? 'glob' : 'name'; + selectors.push(`.${escapeCSS(name)}-${kind}-folder-icon`); addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); result.hasFolderIcons = true; } @@ -303,7 +304,8 @@ export class FileIconThemeLoader { for (const key in folderNamesExpanded) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(name)}-name-folder-icon`); + const kind = name.indexOf('*') !== -1 ? 'glob' : 'name'; + selectors.push(`.${escapeCSS(name)}-${kind}-folder-icon`); addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } @@ -329,7 +331,8 @@ export class FileIconThemeLoader { const segments = name.split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); + const kind = name.indexOf('*') !== -1 ? 'glob' : 'ext'; + selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-${kind}-file-icon`); } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } @@ -343,12 +346,14 @@ export class FileIconThemeLoader { for (const key in fileNames) { const selectors: string[] = []; const fileName = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(fileName)}-name-file-icon`); + const kind = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; + selectors.push(`.${escapeCSS(fileName)}-${kind}-file-icon`); selectors.push('.name-file-icon'); // extra segment to increase file-name score const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); + const kind = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; + selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-${kind}-file-icon`); } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } From e0e91237e521861fbab794f05f9ce6f1f1ecedc7 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Feb 2023 22:06:52 +0000 Subject: [PATCH 02/37] Fix CI compile error --- src/vs/editor/common/services/getIconClasses.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 0cc791fde6ffa..8c87337f09c77 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -41,7 +41,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Folders if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); - pushGlobIconClasses(classes, name); + pushGlobIconClasses(classes, name, 'folder'); } // Files @@ -61,7 +61,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } } classes.push(`ext-file-icon`); // extra segment to increase file-ext score - pushGlobIconClasses(classes, name); + pushGlobIconClasses(classes, name, 'file'); } // Detected Mode From e487bd1530d8fdd2264aa37cdd349dba258b0971 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 08:58:11 +0000 Subject: [PATCH 03/37] Fix post-glob ext file icon classes (#174286) --- .../workbench/services/themes/browser/fileIconThemeData.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index be321ddbe7002..e46026e406806 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -331,8 +331,7 @@ export class FileIconThemeLoader { const segments = name.split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { - const kind = name.indexOf('*') !== -1 ? 'glob' : 'ext'; - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-${kind}-file-icon`); + selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } @@ -352,8 +351,7 @@ export class FileIconThemeLoader { const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { - const kind = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-${kind}-file-icon`); + selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } From b88fead89be2039de2b4356abf1b8935a994f3a1 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:24:13 +0000 Subject: [PATCH 04/37] Fix coaelesced glob regex --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 8c87337f09c77..1b85326360cc7 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -12,7 +12,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { FileKind } from 'vs/platform/files/common/files'; const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; -const fileIconCoalescedGlobRegex = /(?<=\*)(?:\.\*)+/; +const fileIconCoalescedGlobRegex = /\*(?:\.\*)+/; export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind): string[] { From fe76b5f16bb96bcd49a3f41e2de5c30a48d2d077 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:41:33 +0000 Subject: [PATCH 05/37] Narrow folder name type in getIconClasses --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 1b85326360cc7..1db2fe9177ae8 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -39,7 +39,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } // Folders - if (fileKind === FileKind.FOLDER) { + if (fileKind === FileKind.FOLDER && name) { classes.push(`${name}-name-folder-icon`); pushGlobIconClasses(classes, name, 'folder'); } From c022fc6105afd18a03a36fefdfe0c7f0709f333a Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:56:29 +0000 Subject: [PATCH 06/37] Polishing --- .../editor/common/services/getIconClasses.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 1db2fe9177ae8..c3a2878cb8dac 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -39,9 +39,11 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } // Folders - if (fileKind === FileKind.FOLDER && name) { + if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); - pushGlobIconClasses(classes, name, 'folder'); + if (name && name.length <= 255) { + pushGlobIconClassesForName(name, classes, 'folder'); // add globs targeting file name + } } // Files @@ -55,13 +57,13 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // (most file systems do not allow files > 255 length) with lots of `.` characters // https://github.com/microsoft/vscode/issues/116199 if (name.length <= 255) { + pushGlobIconClassesForName(name, classes, 'file'); // add globs targeting file name const segments = name.split('.'); 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 - pushGlobIconClasses(classes, name, 'file'); } // Detected Mode @@ -78,13 +80,12 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } -// Generate globs matching file icon themes -function pushGlobIconClasses(classes: string[], name: string, kind: string) { +function pushGlobIconClassesForName(name: string, classes: string[], kind: string) { // Remove ellipsis to defend against explosive combination const segments = name.replace(/\.\.\.+/g, '').split('.'); - // Limit permutative ("full") glob generation to <=4 file extensions. - if (segments.length < 5) { + // Limit permutative ("full") glob generation to 4 dot segments (<=3 file extensions). + if (segments.length <= 4) { const bitmask = Math.pow(2, segments.length) - 1; // All globs excluding those with chained `*` dot segments @@ -105,7 +106,9 @@ function pushGlobIconClasses(classes: string[], name: string, kind: string) { // Globs including chained * dot segments classes.push(`${glob}-glob-${kind}-icon`); } - } else { + } + + if (segments.length >= 5) { const lastDotIndex = segments.length - 1; for (let i = 0; i < lastDotIndex; i++) { From f4e9f33c0fbf881b14f892ce09985c67ae62fa2b Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 13:06:54 +0000 Subject: [PATCH 07/37] Update CSS glob comments post-polish --- src/vs/editor/common/services/getIconClasses.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index c3a2878cb8dac..8ed4e8a1d824d 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -84,7 +84,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin // Remove ellipsis to defend against explosive combination const segments = name.replace(/\.\.\.+/g, '').split('.'); - // Limit permutative ("full") glob generation to 4 dot segments (<=3 file extensions). + // Permutative ("full") file glob generation, limited to 4 dot segments (<=3 file extensions) if (segments.length <= 4) { const bitmask = Math.pow(2, segments.length) - 1; @@ -108,6 +108,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin } } + // Prefix matching and wildcard matching glob generation, for >=5 dot segments (>=4 file extensions) if (segments.length >= 5) { const lastDotIndex = segments.length - 1; From 9eb51e016dcd63989cca28e54dcb42d966206603 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 13:13:03 +0000 Subject: [PATCH 08/37] Typo: file -> folder --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 8ed4e8a1d824d..52b314ba75e98 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -42,7 +42,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); if (name && name.length <= 255) { - pushGlobIconClassesForName(name, classes, 'folder'); // add globs targeting file name + pushGlobIconClassesForName(name, classes, 'folder'); // add globs targeting folder name } } From eda562c4cc9bc887bb13281a84fc91cb556a19ae Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 13:22:21 +0000 Subject: [PATCH 09/37] Polish up variable names --- src/vs/editor/common/services/getIconClasses.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 52b314ba75e98..56c18329f9f82 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -89,11 +89,11 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin const bitmask = Math.pow(2, segments.length) - 1; // All globs excluding those with chained `*` dot segments - for (let i = 0; i < bitmask; i++) { + for (let permutation = 0; permutation < bitmask; permutation++) { let buffer = []; - for (let j = 0; j < segments.length; j++) { - const base = Math.pow(2, j); - buffer.push(i & base ? segments[j] : '*'); + for (let exponent = 0; exponent < segments.length; exponent++) { + const base = Math.pow(2, exponent); + buffer.push(permutation & base ? segments[exponent] : '*'); } const glob = buffer.join('.'); From b83b446a281d36a096af42b2f1068e1bfb2ae094 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 13:46:52 +0000 Subject: [PATCH 10/37] let -> const --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 56c18329f9f82..26e27e4c51d2e 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -90,7 +90,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin // All globs excluding those with chained `*` dot segments for (let permutation = 0; permutation < bitmask; permutation++) { - let buffer = []; + const buffer = []; for (let exponent = 0; exponent < segments.length; exponent++) { const base = Math.pow(2, exponent); buffer.push(permutation & base ? segments[exponent] : '*'); From 24c081f6d79778d30abe71fe9fc1d947f7b9c4d4 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 16:26:29 +0000 Subject: [PATCH 11/37] Reduce glob icon priority of using name score classes Refs: [#issuecomment-1429874059](github.com/microsoft/vscode/pull/174286#issuecomment-1429874059) --- .../editor/common/services/getIconClasses.ts | 1 + .../themes/browser/fileIconThemeData.ts | 36 ++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 26e27e4c51d2e..775f2d2b1166d 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -41,6 +41,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Folders if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); + classes.push(`name-folder-icon`); // extra segment to increase folder-name score if (name && name.length <= 255) { pushGlobIconClassesForName(name, classes, 'folder'); // add globs targeting folder name } diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index e46026e406806..08dbbbb9ac9d0 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -293,9 +293,16 @@ export class FileIconThemeLoader { for (const key in folderNames) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors); - const kind = name.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(name)}-${kind}-folder-icon`); - addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); + const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; + + selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); + + if (mode === 'name') { + selectors.push('.name-folder-icon'); + } + + addSelector(`${qualifier} ${selectors.join('')}::before`, folderNames[key]); + result.hasFolderIcons = true; } } @@ -304,9 +311,15 @@ export class FileIconThemeLoader { for (const key in folderNamesExpanded) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors); - const kind = name.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(name)}-${kind}-folder-icon`); - addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); + const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; + + selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); + + if (mode === 'name') { + selectors.push('.folder-icon'); + } + + addSelector(`${qualifier} ${expanded} ${selectors.join('')}::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } } @@ -328,6 +341,7 @@ export class FileIconThemeLoader { for (const key in fileExtensions) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors); + const segments = name.split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { @@ -335,7 +349,9 @@ export class FileIconThemeLoader { } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } + addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]); + result.hasFileIcons = true; hasSpecificFileIcons = true; } @@ -345,9 +361,11 @@ export class FileIconThemeLoader { for (const key in fileNames) { const selectors: string[] = []; const fileName = handleParentFolder(key.toLowerCase(), selectors); - const kind = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(fileName)}-${kind}-file-icon`); + const mode = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; + + selectors.push(`.${escapeCSS(fileName)}-${mode}-file-icon`); selectors.push('.name-file-icon'); // extra segment to increase file-name score + const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { @@ -355,7 +373,9 @@ export class FileIconThemeLoader { } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } + addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]); + result.hasFileIcons = true; hasSpecificFileIcons = true; } From e34c92d93d9f20c48fa61cd8e4a913a4161649f8 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 16:45:45 +0000 Subject: [PATCH 12/37] Cleanup pass on CSS glob comments --- src/vs/editor/common/services/getIconClasses.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 775f2d2b1166d..617e0f88e8af6 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -12,7 +12,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { FileKind } from 'vs/platform/files/common/files'; const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; -const fileIconCoalescedGlobRegex = /\*(?:\.\*)+/; +const fileIconWildcardChainRegex = /\*(?:\.\*)+/; export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind): string[] { @@ -89,7 +89,6 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin if (segments.length <= 4) { const bitmask = Math.pow(2, segments.length) - 1; - // All globs excluding those with chained `*` dot segments for (let permutation = 0; permutation < bitmask; permutation++) { const buffer = []; for (let exponent = 0; exponent < segments.length; exponent++) { @@ -99,8 +98,8 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin const glob = buffer.join('.'); // Globs with chained * filename segments coaelesced into ** - if (glob.match(fileIconCoalescedGlobRegex)) { - const coaelescedGlob = glob.replace(fileIconCoalescedGlobRegex, '**'); + if (glob.match(fileIconWildcardChainRegex)) { + const coaelescedGlob = glob.replace(fileIconWildcardChainRegex, '**'); classes.push(`${coaelescedGlob}-glob-${kind}-icon`); } From 7d0f02a34d1f83e4b800bbcd9efcfcb8ab5b0afd Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 17:01:01 +0000 Subject: [PATCH 13/37] Fix regression (folder-specifc icons mixing with files) --- .../workbench/services/themes/browser/fileIconThemeData.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 08dbbbb9ac9d0..7ba50a5006928 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -301,7 +301,7 @@ export class FileIconThemeLoader { selectors.push('.name-folder-icon'); } - addSelector(`${qualifier} ${selectors.join('')}::before`, folderNames[key]); + addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); result.hasFolderIcons = true; } @@ -316,10 +316,10 @@ export class FileIconThemeLoader { selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); if (mode === 'name') { - selectors.push('.folder-icon'); + selectors.push('.name-folder-icon'); } - addSelector(`${qualifier} ${expanded} ${selectors.join('')}::before`, folderNamesExpanded[key]); + addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } } From 35cded56be3485a98313db0d5d9d3a318158b641 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 18:40:08 +0000 Subject: [PATCH 14/37] Remove switch for dash separator --- src/vs/editor/common/services/getIconClasses.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 617e0f88e8af6..7def61178f971 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -138,14 +138,6 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin const basename = name.substring(name.lastIndexOf('.', dotLastIndex - 1), dotLastIndex); let separator = basename.match(/_|-/)?.[0]; - switch (true) { - case basename.indexOf('_') > -1: - separator = '_'; - break; - case basename.indexOf('-') > -1: - separator = '-'; - break; - } if (separator) { // Prefix basename glob e.g. `test_*.py` From 277940ebea46974a6e63f92d1038ac34e1971ca0 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:00:59 +0000 Subject: [PATCH 15/37] let -> const --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 7def61178f971..4d94f4d411e3c 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -137,7 +137,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin const extname = name.substring(dotLastIndex); const basename = name.substring(name.lastIndexOf('.', dotLastIndex - 1), dotLastIndex); - let separator = basename.match(/_|-/)?.[0]; + const separator = basename.match(/_|-/)?.[0]; if (separator) { // Prefix basename glob e.g. `test_*.py` From 34a982f49d801d11a2cfea3dbd5d26efc33b5961 Mon Sep 17 00:00:00 2001 From: "Z Mohamed (CT-TAE)" <114668551+zm-cttae@users.noreply.github.com> Date: Thu, 16 Feb 2023 09:23:07 +0000 Subject: [PATCH 16/37] Cleanup pass --- .../editor/common/services/getIconClasses.ts | 29 ++++++++++--------- .../themes/browser/fileIconThemeData.ts | 10 +++++-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 4d94f4d411e3c..e6620e48b2ddb 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -38,12 +38,20 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } } + // 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 (name && name.length <= 255) { + segments = name.replace(/\.\.\.+/g, '').split('.'); + } + // Folders if (fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); classes.push(`name-folder-icon`); // extra segment to increase folder-name score - if (name && name.length <= 255) { - pushGlobIconClassesForName(name, classes, 'folder'); // add globs targeting folder name + if (name && segments) { + pushGlobIconClassesForName(name, classes, segments, 'folder'); // add globs targeting folder name } } @@ -54,12 +62,8 @@ export function getIconClasses(modelService: IModelService, languageService: ILa if (name) { classes.push(`${name}-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) { - pushGlobIconClassesForName(name, classes, 'file'); // add globs targeting file name - const segments = name.split('.'); + if (segments) { + pushGlobIconClassesForName(name, classes, segments, 'file'); // add globs targeting file name 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 } @@ -81,10 +85,7 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } -function pushGlobIconClassesForName(name: string, classes: string[], kind: string) { - // Remove ellipsis to defend against explosive combination - const segments = name.replace(/\.\.\.+/g, '').split('.'); - +function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string) { // Permutative ("full") file glob generation, limited to 4 dot segments (<=3 file extensions) if (segments.length <= 4) { const bitmask = Math.pow(2, segments.length) - 1; @@ -127,8 +128,8 @@ function pushGlobIconClassesForName(name: string, classes: string[], kind: strin } } - // Simplest-case globs for dashed file basenames. - // Targets tooling filename conventions. + // Globs for dashed file basenames (1 file extension) + // Targets prefix or suffix, e.g. the tooling filename conventions `test_*.py` & `*_test.go` const dotLastIndex = name.lastIndexOf('.'); if ( dotLastIndex !== -1 && // >=1 file extensions diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 7ba50a5006928..e31b9f869695f 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -298,7 +298,7 @@ export class FileIconThemeLoader { selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); if (mode === 'name') { - selectors.push('.name-folder-icon'); + selectors.push('.name-folder-icon'); // extra segment to increase folder-name score } addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); @@ -306,6 +306,7 @@ export class FileIconThemeLoader { result.hasFolderIcons = true; } } + const folderNamesExpanded = associations.folderNamesExpanded; if (folderNamesExpanded) { for (const key in folderNamesExpanded) { @@ -316,7 +317,7 @@ export class FileIconThemeLoader { selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); if (mode === 'name') { - selectors.push('.name-folder-icon'); + selectors.push('.name-folder-icon'); // extra segment to increase folder-name score } addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); @@ -364,7 +365,10 @@ export class FileIconThemeLoader { const mode = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; selectors.push(`.${escapeCSS(fileName)}-${mode}-file-icon`); - selectors.push('.name-file-icon'); // extra segment to increase file-name score + + if (mode === 'name') { + selectors.push('.name-file-icon'); // extra segment to increase file-name score + } const segments = fileName.split('.'); if (segments.length) { From 917618bd9a7b57368b4eaa44a2095cbf45685210 Mon Sep 17 00:00:00 2001 From: ZM <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 17 Feb 2023 09:38:54 +0000 Subject: [PATCH 17/37] Apply "1 file extension" restriction to dash/underscore globs --- src/vs/editor/common/services/getIconClasses.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index e6620e48b2ddb..dcd6d6d724c93 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -130,13 +130,10 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s // Globs for dashed file basenames (1 file extension) // Targets prefix or suffix, e.g. the tooling filename conventions `test_*.py` & `*_test.go` - const dotLastIndex = name.lastIndexOf('.'); - if ( - dotLastIndex !== -1 && // >=1 file extensions - dotLastIndex === name.indexOf('.') // <=1 file extension - ) { - const extname = name.substring(dotLastIndex); - const basename = name.substring(name.lastIndexOf('.', dotLastIndex - 1), dotLastIndex); + if (segments.length === 1) { + const dotIndex = name.indexOf('.'); + const extname = name.substring(dotIndex); + const basename = name.substring(0, dotIndex); const separator = basename.match(/_|-/)?.[0]; From d0ec167e8c01ef684064f4c3686c756ce613552e Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:37:03 +0000 Subject: [PATCH 18/37] [nit] use icon kind in dirname per #136656 Make it clear where the dirname class is contributed from. --- src/vs/editor/common/services/getIconClasses.ts | 5 +++-- .../services/themes/browser/fileIconThemeData.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index dcd6d6d724c93..4be8a43adb067 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -17,7 +17,8 @@ const fileIconWildcardChainRegex = /\*(?:\.\*)+/; export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind): string[] { // 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) { // Get the path and name of the resource. For data-URIs, we need to parse specially @@ -30,7 +31,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa if (match) { name = cssEscape(match[2].toLowerCase()); if (match[1]) { - classes.push(`${cssEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory + classes.push(`${cssEscape(match[1].toLowerCase())}-dirname-${kindClass}`); // parent directory } } else { diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index e31b9f869695f..ca81b9809d5f8 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -292,7 +292,7 @@ export class FileIconThemeLoader { if (folderNames) { for (const key in folderNames) { const selectors: string[] = []; - const name = handleParentFolder(key.toLowerCase(), selectors); + const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); @@ -311,7 +311,7 @@ export class FileIconThemeLoader { if (folderNamesExpanded) { for (const key in folderNamesExpanded) { const selectors: string[] = []; - const name = handleParentFolder(key.toLowerCase(), selectors); + const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); @@ -341,7 +341,7 @@ export class FileIconThemeLoader { if (fileExtensions) { for (const key in fileExtensions) { const selectors: string[] = []; - const name = handleParentFolder(key.toLowerCase(), selectors); + const name = handleParentFolder(key.toLowerCase(), selectors, 'file'); const segments = name.split('.'); if (segments.length) { @@ -361,7 +361,7 @@ export class FileIconThemeLoader { if (fileNames) { for (const key in fileNames) { const selectors: string[] = []; - const fileName = handleParentFolder(key.toLowerCase(), selectors); + const fileName = handleParentFolder(key.toLowerCase(), selectors, 'file'); const mode = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; selectors.push(`.${escapeCSS(fileName)}-${mode}-file-icon`); @@ -461,11 +461,11 @@ export class FileIconThemeLoader { } -function handleParentFolder(key: string, selectors: string[]): string { +function handleParentFolder(key: string, selectors: string[], kind: string): string { const lastIndexOfSlash = key.lastIndexOf('/'); if (lastIndexOfSlash >= 0) { const parentFolder = key.substring(0, lastIndexOfSlash); - selectors.push(`.${escapeCSS(parentFolder)}-name-dir-icon`); + selectors.push(`.${escapeCSS(parentFolder)}-dirname-${kind}-icon`); return key.substring(lastIndexOfSlash + 1); } return key; From 5c5ec596f4b91d2f9bde9c4a6a51faf5fc1b1aa0 Mon Sep 17 00:00:00 2001 From: "Zee (ZM)" <114668551+zm-cttae@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:22:57 +0000 Subject: [PATCH 19/37] Use `*` (non-coalescing wildcard) to match multiple dots This commit brings consistency with `vscode.GlobPattern` API. Refs: #174286 @aeschli review --- src/vs/editor/common/services/getIconClasses.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 4be8a43adb067..c3c0d7aed0f73 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -99,9 +99,9 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s } const glob = buffer.join('.'); - // Globs with chained * filename segments coaelesced into ** + // Globs with chained * filename segments coaelesced into * if (glob.match(fileIconWildcardChainRegex)) { - const coaelescedGlob = glob.replace(fileIconWildcardChainRegex, '**'); + const coaelescedGlob = glob.replace(fileIconWildcardChainRegex, '*'); classes.push(`${coaelescedGlob}-glob-${kind}-icon`); } @@ -117,7 +117,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s for (let i = 0; i < lastDotIndex; i++) { // Prefix-matching coalescing globs const suffixSegments = segments.slice(0, i + 1); - suffixSegments.push(i < lastDotIndex - 1 ? '**' : '*'); + suffixSegments.push('*'); const suffixGlob = suffixSegments.join('.'); classes.push(`${suffixGlob}-glob-${kind}-icon`); From 6ebfb663210c70883dcc91d884df3a1acf35205b Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:07:10 +0000 Subject: [PATCH 20/37] Remove permutative and wildcard chain globs This limits the spec to: - `non-coalescing-wildcard` - `basename-prefix-with-undescore` - `basename-suffix-with-undescore` Refs: #174286 --- .../editor/common/services/getIconClasses.ts | 90 +++++++------------ 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index c3c0d7aed0f73..b5750903978a0 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -12,7 +12,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { FileKind } from 'vs/platform/files/common/files'; const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; -const fileIconWildcardChainRegex = /\*(?:\.\*)+/; export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind): string[] { @@ -87,71 +86,42 @@ export function getIconClassesForLanguageId(languageId: string): string[] { } function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string) { - // Permutative ("full") file glob generation, limited to 4 dot segments (<=3 file extensions) - if (segments.length <= 4) { - const bitmask = Math.pow(2, segments.length) - 1; - - for (let permutation = 0; permutation < bitmask; permutation++) { - const buffer = []; - for (let exponent = 0; exponent < segments.length; exponent++) { - const base = Math.pow(2, exponent); - buffer.push(permutation & base ? segments[exponent] : '*'); - } - const glob = buffer.join('.'); - - // Globs with chained * filename segments coaelesced into * - if (glob.match(fileIconWildcardChainRegex)) { - const coaelescedGlob = glob.replace(fileIconWildcardChainRegex, '*'); - classes.push(`${coaelescedGlob}-glob-${kind}-icon`); - } - - // Globs including chained * dot segments - classes.push(`${glob}-glob-${kind}-icon`); - } + // Non-coalescing wildcard globs + for (let i = 0; i < segments.length; i++) { + const baseSegments = segments.slice(); + baseSegments[i] = '*'; + const baseGlob = baseSegments.join('.'); + classes.push(`${baseGlob}-glob-${kind}-icon`); } - // Prefix matching and wildcard matching glob generation, for >=5 dot segments (>=4 file extensions) - if (segments.length >= 5) { - const lastDotIndex = segments.length - 1; - - for (let i = 0; i < lastDotIndex; i++) { - // Prefix-matching coalescing globs - const suffixSegments = segments.slice(0, i + 1); - suffixSegments.push('*'); - const suffixGlob = suffixSegments.join('.'); - classes.push(`${suffixGlob}-glob-${kind}-icon`); - - // Non-coalescing wildcard globs - const baseSegments = segments.slice(); - baseSegments[i] = '*'; - const baseGlob = baseSegments.join('.'); - classes.push(`${baseGlob}-glob-${kind}-icon`); - } + if (segments.length !== 1) { + return; } // Globs for dashed file basenames (1 file extension) - // Targets prefix or suffix, e.g. the tooling filename conventions `test_*.py` & `*_test.go` - if (segments.length === 1) { - const dotIndex = name.indexOf('.'); - const extname = name.substring(dotIndex); - const basename = name.substring(0, dotIndex); - - const separator = basename.match(/_|-/)?.[0]; - - if (separator) { - // Prefix basename glob e.g. `test_*.py` - const basenameDashIndex = basename.indexOf(separator); - const basenamePrefix = basename.substring(0, basenameDashIndex); - const basenamePrefixGlob = basenamePrefix + separator + '*' + extname; - classes.push(`${basenamePrefixGlob}-glob-${kind}-icon`); - - // Suffix basename glob e.g. `*_test.go` - const basenameLastDashIndex = basename.lastIndexOf(separator); - const basenameSuffix = basename.substring(basenameLastDashIndex + 1); - const basenameSuffixGlob = '*' + separator + basenameSuffix + extname; - classes.push(`${basenameSuffixGlob}-glob-${kind}-icon`); - } + // Targets hyphenated prefix or suffix + // E.g. the tooling filename conventions `test_*.py` & `*_test.go` + const dotIndex = name.indexOf('.'); + const extname = name.substring(dotIndex); + const basename = name.substring(0, dotIndex); + + const separator = basename.match(/_|-/)?.[0]; + + if (!separator) { + return; } + + // Prefix basename glob e.g. `test_*.py` + const basenameDashIndex = basename.indexOf(separator); + const basenamePrefix = basename.substring(0, basenameDashIndex); + const basenamePrefixGlob = basenamePrefix + separator + '*' + extname; + classes.push(`${basenamePrefixGlob}-glob-${kind}-icon`); + + // Suffix basename glob e.g. `*_test.go` + const basenameLastDashIndex = basename.lastIndexOf(separator); + const basenameSuffix = basename.substring(basenameLastDashIndex + 1); + const basenameSuffixGlob = '*' + separator + basenameSuffix + extname; + classes.push(`${basenameSuffixGlob}-glob-${kind}-icon`); } function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null { From d06ed114ecfe8150144e0cb27bfbf2f5d4c0a673 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:30:34 +0000 Subject: [PATCH 21/37] Ban icon globs for >=5 filename dot segments Refs: #174286 --- src/vs/editor/common/services/getIconClasses.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index b5750903978a0..8c6bb3ec34f87 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -47,10 +47,10 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } // Folders - if (fileKind === FileKind.FOLDER) { + if (name && fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); classes.push(`name-folder-icon`); // extra segment to increase folder-name score - if (name && segments) { + if (name.length <= 255 && segments && segments.length <= 4) { pushGlobIconClassesForName(name, classes, segments, 'folder'); // add globs targeting folder name } } @@ -62,7 +62,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa if (name) { classes.push(`${name}-name-file-icon`); classes.push(`name-file-icon`); // extra segment to increase file-name score - if (segments) { + if (name.length <= 255 && segments && segments.length <= 4) { pushGlobIconClassesForName(name, classes, segments, 'file'); // add globs targeting file name 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 From 95b759f3e52ebf4cdea3ac2e22c28cb3fc8ebf17 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:51:43 +0000 Subject: [PATCH 22/37] Note limit in filename extension comments --- src/vs/editor/common/services/getIconClasses.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 8c6bb3ec34f87..6a064eab922b9 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -86,7 +86,7 @@ export function getIconClassesForLanguageId(languageId: string): string[] { } function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string) { - // Non-coalescing wildcard globs + // Non-coalescing wildcard globs, limited to <=4 dot segments (<=3 file extensions). for (let i = 0; i < segments.length; i++) { const baseSegments = segments.slice(); baseSegments[i] = '*'; @@ -98,7 +98,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s return; } - // Globs for dashed file basenames (1 file extension) + // Globs for dashed file basenames, limited to 2 dot segments (1 file extension). // Targets hyphenated prefix or suffix // E.g. the tooling filename conventions `test_*.py` & `*_test.go` const dotIndex = name.indexOf('.'); @@ -111,13 +111,13 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s return; } - // Prefix basename glob e.g. `test_*.py` + // Prefix basename glob for 1 file extension, e.g. `test_*.py`. const basenameDashIndex = basename.indexOf(separator); const basenamePrefix = basename.substring(0, basenameDashIndex); const basenamePrefixGlob = basenamePrefix + separator + '*' + extname; classes.push(`${basenamePrefixGlob}-glob-${kind}-icon`); - // Suffix basename glob e.g. `*_test.go` + // Suffix basename glob for 1 file extension, e.g. `*_test.go`. const basenameLastDashIndex = basename.lastIndexOf(separator); const basenameSuffix = basename.substring(basenameLastDashIndex + 1); const basenameSuffixGlob = '*' + separator + basenameSuffix + extname; From 64be5501d5de6ca8b7a0261c4e55cb6e5ffc2dda Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Mar 2023 07:45:44 +0000 Subject: [PATCH 23/37] Limit icon glob wildcard to file extensions (>=2nd segment) Refs: #174286 --- src/vs/editor/common/services/getIconClasses.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 6a064eab922b9..64204e191b229 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -85,11 +85,12 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } -function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string) { +function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string): void { // Non-coalescing wildcard globs, limited to <=4 dot segments (<=3 file extensions). - for (let i = 0; i < segments.length; i++) { + // We start from the 2nd segment (index=1) to prevent overlap with extension. + for (let index = 1; index < segments.length; index++) { const baseSegments = segments.slice(); - baseSegments[i] = '*'; + baseSegments[index] = '*'; const baseGlob = baseSegments.join('.'); classes.push(`${baseGlob}-glob-${kind}-icon`); } From 5875d0aaf3171962950e1037ccad0abe4f87dfff Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Mar 2023 07:59:53 +0000 Subject: [PATCH 24/37] Fix dash separated segment globs --- src/vs/editor/common/services/getIconClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 64204e191b229..3a2a61a5bf04e 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -95,7 +95,7 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s classes.push(`${baseGlob}-glob-${kind}-icon`); } - if (segments.length !== 1) { + if (segments.length !== 2) { return; } From 6612b618ee0fb2e195310f7eaa0aa33eeda8e2bb Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Mar 2023 08:23:00 +0000 Subject: [PATCH 25/37] [nit] Move basenameGlob comment above #segments!=2 if-return --- src/vs/editor/common/services/getIconClasses.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 3a2a61a5bf04e..ac72a3467fd2c 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -87,7 +87,7 @@ export function getIconClassesForLanguageId(languageId: string): string[] { function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string): void { // Non-coalescing wildcard globs, limited to <=4 dot segments (<=3 file extensions). - // We start from the 2nd segment (index=1) to prevent overlap with extension. + // We start from the 2nd segment (index=1) to prevent overlap with Extension(s). for (let index = 1; index < segments.length; index++) { const baseSegments = segments.slice(); baseSegments[index] = '*'; @@ -95,13 +95,13 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s classes.push(`${baseGlob}-glob-${kind}-icon`); } + // Globs for dashed file basenames, limited to 2 dot segments (1 file extension). + // Targets hyphenated prefix or suffix + // E.g. the tooling filename conventions `test_*.py` & `*_test.go` if (segments.length !== 2) { return; } - // Globs for dashed file basenames, limited to 2 dot segments (1 file extension). - // Targets hyphenated prefix or suffix - // E.g. the tooling filename conventions `test_*.py` & `*_test.go` const dotIndex = name.indexOf('.'); const extname = name.substring(dotIndex); const basename = name.substring(0, dotIndex); From 390248a87b777b131a71079d87bf4885bd73443a Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Mon, 13 Mar 2023 16:22:54 +0000 Subject: [PATCH 26/37] [nit] Test typeof filename is string for icon classes --- src/vs/editor/common/services/getIconClasses.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index ac72a3467fd2c..a08097d1c7690 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -42,12 +42,12 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // (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 (name && name.length <= 255) { + if (typeof name === 'string' && name.length <= 255) { segments = name.replace(/\.\.\.+/g, '').split('.'); } // Folders - if (name && fileKind === FileKind.FOLDER) { + if (typeof name === 'string' && fileKind === FileKind.FOLDER) { classes.push(`${name}-name-folder-icon`); classes.push(`name-folder-icon`); // extra segment to increase folder-name score if (name.length <= 255 && segments && segments.length <= 4) { @@ -59,7 +59,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa else { // Name & Extension(s) - if (name) { + if (typeof name === 'string') { classes.push(`${name}-name-file-icon`); classes.push(`name-file-icon`); // extra segment to increase file-name score if (name.length <= 255 && segments && segments.length <= 4) { From 62567401cc280a59ab025f96470d9852721e19f7 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Wed, 15 Mar 2023 10:20:37 +0000 Subject: [PATCH 27/37] [nit] Rename base-glob vars to wildcard-glob --- src/vs/editor/common/services/getIconClasses.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index a08097d1c7690..e6713b7cdb829 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -89,10 +89,10 @@ function pushGlobIconClassesForName(name: string, classes: string[], segments: s // Non-coalescing wildcard globs, limited to <=4 dot segments (<=3 file extensions). // We start from the 2nd segment (index=1) to prevent overlap with Extension(s). for (let index = 1; index < segments.length; index++) { - const baseSegments = segments.slice(); - baseSegments[index] = '*'; - const baseGlob = baseSegments.join('.'); - classes.push(`${baseGlob}-glob-${kind}-icon`); + const wildcardSegments = segments.slice(); + wildcardSegments[index] = '*'; + const wildcardGlob = wildcardSegments.join('.'); + classes.push(`${wildcardGlob}-glob-${kind}-icon`); } // Globs for dashed file basenames, limited to 2 dot segments (1 file extension). From 0e877f164f622a6979f4b48d7ccfda3fb1e51cc5 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:14:49 +0000 Subject: [PATCH 28/37] WIP - implement simpler file icon glob assocations (#177650) --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 8 ++ .../editor/common/services/getIconClasses.ts | 106 ++++++++---------- .../themes/browser/fileIconThemeData.ts | 52 +++++++-- 3 files changed, 93 insertions(+), 73 deletions(-) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 980be904fa46c..e6027a7aea996 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -25,6 +25,7 @@ export interface IIconLabelValueOptions { descriptionTitle?: string; hideIcon?: boolean; extraClasses?: readonly string[]; + extraAttributes?: { [attrName: string]: string }; italic?: boolean; strikethrough?: boolean; matches?: readonly IMatch[]; @@ -120,6 +121,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) { @@ -137,6 +139,11 @@ export class IconLabel extends Disposable { if (options.disabledCommand) { containerClasses.push('disabled'); } + + if (options.extraAttributes) { + Object.assign(dataAttributes, options.extraAttributes); + } + if (options.title) { ariaLabel += options.title; } @@ -144,6 +151,7 @@ export class IconLabel extends Disposable { this.domNode.className = labelClasses.join(' '); this.domNode.element.setAttribute('aria-label', ariaLabel); + Object.assign(this.domNode.element.dataset, dataAttributes); this.labelContainer.className = containerClasses.join(' '); 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 e6713b7cdb829..19f075f35706a 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -6,6 +6,7 @@ import { Schemas } from 'vs/base/common/network'; import { DataUri } from 'vs/base/common/resources'; import { URI as uri } from 'vs/base/common/uri'; +import { extname, basename } from 'vs/base/common/path'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; @@ -19,51 +20,34 @@ export function getIconClasses(modelService: IModelService, languageService: ILa 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 = cssEscape(match[2].toLowerCase()); - if (match[1]) { - classes.push(`${cssEscape(match[1].toLowerCase())}-dirname-${kindClass}`); // parent directory - } - - } else { - name = cssEscape(resource.authority.toLowerCase()); - } + if (dirname) { + classes.push(`${dirname}-dirname-${kindClass}`); // parent directory } // 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 name === 'string' && name.length <= 255) { - segments = name.replace(/\.\.\.+/g, '').split('.'); + if (typeof filename === 'string' && filename.length <= 255) { + segments = filename.replace(/\.\.\.+/g, '').split('.'); } // Folders - if (typeof name === 'string' && 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 - if (name.length <= 255 && segments && segments.length <= 4) { - pushGlobIconClassesForName(name, classes, segments, 'folder'); // add globs targeting folder name - } } // Files else { // Name & Extension(s) - if (typeof name === 'string') { - 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 - if (name.length <= 255 && segments && segments.length <= 4) { - pushGlobIconClassesForName(name, classes, segments, 'file'); // add globs targeting file name + 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 } @@ -85,44 +69,21 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } -function pushGlobIconClassesForName(name: string, classes: string[], segments: string[], kind: string): void { - // Non-coalescing wildcard globs, limited to <=4 dot segments (<=3 file extensions). - // We start from the 2nd segment (index=1) to prevent overlap with Extension(s). - for (let index = 1; index < segments.length; index++) { - const wildcardSegments = segments.slice(); - wildcardSegments[index] = '*'; - const wildcardGlob = wildcardSegments.join('.'); - classes.push(`${wildcardGlob}-glob-${kind}-icon`); - } +export function getIconAttributes(resource: uri | undefined, fileKind?: FileKind) { + const attributes: Record = {}; - // Globs for dashed file basenames, limited to 2 dot segments (1 file extension). - // Targets hyphenated prefix or suffix - // E.g. the tooling filename conventions `test_*.py` & `*_test.go` - if (segments.length !== 2) { - return; - } - - const dotIndex = name.indexOf('.'); - const extname = name.substring(dotIndex); - const basename = name.substring(0, dotIndex); - - const separator = basename.match(/_|-/)?.[0]; + if (resource) { + const { filename } = getResourceName(resource); - if (!separator) { - return; + if (filename) { + const fileExtname = extname(filename); + const fileBasename = basename(filename, fileExtname); + attributes.fileIconExtname = fileExtname.substring(1); + attributes.fileIconBasename = fileBasename; + } } - // Prefix basename glob for 1 file extension, e.g. `test_*.py`. - const basenameDashIndex = basename.indexOf(separator); - const basenamePrefix = basename.substring(0, basenameDashIndex); - const basenamePrefixGlob = basenamePrefix + separator + '*' + extname; - classes.push(`${basenamePrefixGlob}-glob-${kind}-icon`); - - // Suffix basename glob for 1 file extension, e.g. `*_test.go`. - const basenameLastDashIndex = basename.lastIndexOf(separator); - const basenameSuffix = basename.substring(basenameLastDashIndex + 1); - const basenameSuffixGlob = '*' + separator + basenameSuffix + extname; - classes.push(`${basenameSuffixGlob}-glob-${kind}-icon`); + return attributes; } function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null { @@ -159,6 +120,29 @@ 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) { + dirname = cssEscape(cssEscape(match[1].toLowerCase())); + filename = cssEscape(match[2].toLowerCase()); + + } else { + filename = cssEscape(resource.authority.toLowerCase()); + } + } + + return { filename, dirname }; +} + function cssEscape(str: string): string { return str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. } diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index ca81b9809d5f8..305d8b932f7c7 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -293,14 +293,14 @@ export class FileIconThemeLoader { for (const key in folderNames) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); - const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); - - if (mode === 'name') { + if (!name.includes('*')) { + selectors.push(`.${escapeCSS(name)}-name-folder-icon`); selectors.push('.name-folder-icon'); // extra segment to increase folder-name score } + pushGlobSelectors(name, selectors, 'folder'); + addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); result.hasFolderIcons = true; @@ -312,14 +312,15 @@ export class FileIconThemeLoader { for (const key in folderNamesExpanded) { const selectors: string[] = []; const name = handleParentFolder(key.toLowerCase(), selectors, 'folder'); - const mode = name.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(name)}-${mode}-folder-icon`); - - if (mode === 'name') { + if (!name.includes('*')) { + selectors.push(`.${escapeCSS(name)}-name-folder-icon`); + selectors.push('.folder-icon'); // extra segment to increase folder-name score selectors.push('.name-folder-icon'); // extra segment to increase folder-name score } + pushGlobSelectors(name, selectors, 'folder'); + addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } @@ -362,14 +363,14 @@ export class FileIconThemeLoader { for (const key in fileNames) { const selectors: string[] = []; const fileName = handleParentFolder(key.toLowerCase(), selectors, 'file'); - const mode = fileName.indexOf('*') !== -1 ? 'glob' : 'name'; - selectors.push(`.${escapeCSS(fileName)}-${mode}-file-icon`); - - if (mode === 'name') { + if (!fileName.includes('*')) { + selectors.push(`.${escapeCSS(fileName)}-name-file-icon`); selectors.push('.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++) { @@ -461,6 +462,33 @@ export class FileIconThemeLoader { } +function pushGlobSelectors(name: string, selectors: string[], kind: string): void { + const extname = paths.extname(name); + const basename = paths.basename(name, extname); + selectors.push(escapeCSS(getGlobSelector(basename, kind, 'basename'))); + selectors.push(escapeCSS(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: string[], kind: string): string { const lastIndexOfSlash = key.lastIndexOf('/'); if (lastIndexOfSlash >= 0) { From 1cf48d3a4999bfe3c566103354bb511c3ecc5b2a Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:53:13 +0000 Subject: [PATCH 29/37] Fix missing import --- src/vs/editor/common/services/getIconClasses.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 06103713ac32e..89782a499608d 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 = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; From b5e651df62f25217e22ada79a14faf6feeed837d Mon Sep 17 00:00:00 2001 From: ZM <114668551+zm-cttae@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:35:16 +0000 Subject: [PATCH 30/37] Fix dirname refactor --- src/vs/editor/common/services/getIconClasses.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 89782a499608d..12db50072badb 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -44,7 +44,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Root Folders if (fileKind === FileKind.ROOT_FOLDER) { - classes.push(`${name}-root-name-folder-icon`); + classes.push(`${filename}-root-name-folder-icon`); } // Folders @@ -145,9 +145,10 @@ function getResourceName(resource: uri) { } else { const match = resource.path.match(fileIconDirectoryRegex); if (match) { - dirname = cssEscape(cssEscape(match[1].toLowerCase())); filename = cssEscape(match[2].toLowerCase()); - + if (match[1]) { + dirname = cssEscape(cssEscape(match[1].toLowerCase())); + } } else { filename = cssEscape(resource.authority.toLowerCase()); } From 0d8f64e46454f0067a3e99bb46a4bb52a7cadfa4 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:06:46 +0000 Subject: [PATCH 31/37] Wire up icon attributes for file icon globs --- .../editor/common/services/getIconClasses.ts | 2 +- .../suggest/browser/suggestWidgetRenderer.ts | 9 ++++++++- .../platform/quickinput/common/quickInput.ts | 1 + .../workbench/browser/actions/windowActions.ts | 7 ++++++- .../browser/actions/workspaceCommands.ts | 5 +++-- src/vs/workbench/browser/labels.ts | 12 +++++++++++- .../browser/parts/editor/editorQuickAccess.ts | 3 ++- .../chatMarkdownContentPart.ts | 5 ++++- .../chat/browser/chatInlineAnchorWidget.ts | 5 ++++- .../debug/common/loadedScriptsPicker.ts | 3 ++- .../browser/localHistoryCommands.ts | 5 +++-- .../notebook/browser/controller/editActions.ts | 3 ++- .../preferences/browser/preferencesActions.ts | 3 ++- .../search/browser/anythingQuickAccess.ts | 5 ++++- .../terminal/browser/terminalActions.ts | 5 +++-- .../electron-sandbox/actions/windowActions.ts | 7 +++++-- .../dialogs/browser/simpleFileDialog.ts | 18 +++++++++++++++--- .../common/workspaceExtensionsConfig.ts | 3 ++- 18 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 12db50072badb..25a6a31ba0e44 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -82,7 +82,7 @@ export function getIconClassesForLanguageId(languageId: string): string[] { return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; } -export function getIconAttributes(resource: uri | undefined, fileKind?: FileKind) { +export function getIconAttributes(resource: uri | undefined) { const attributes: Record = {}; if (resource) { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts index 24fb6d3106fc0..73bb611b4bddc 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'; @@ -189,6 +189,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 @@ -198,6 +201,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 e2248c750fbb7..7f5807c95863e 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); } @@ -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..84b8d6acf893f 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; @@ -612,17 +613,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 f2c5ea3bea299..dc30ab5baa032 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -15,7 +15,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 { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; @@ -327,12 +327,15 @@ class CollapsedCodeBlock extends Disposable { const iconText = this.labelService.getUriBasenameLabel(uri); let iconClasses: string[] = []; + let iconAttributes: Record = {}; if (isStreaming) { 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'); diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 669f08b918703..c3da0977fac49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -17,7 +17,7 @@ import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.j import { LanguageFeatureRegistry } from '../../../../editor/common/languageFeatureRegistry.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'; @@ -101,6 +101,7 @@ export class InlineAnchorWidget extends Disposable { let iconText: string; let iconClasses: string[]; + let iconAttributes: Record; let location: { readonly uri: URI; readonly range?: IRange }; let contextMenuId: MenuId; @@ -114,6 +115,7 @@ export class InlineAnchorWidget extends Disposable { iconText = this.data.symbol.name; iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(this.data.symbol.kind))]; + iconAttributes = getIconAttributes(undefined); const providerContexts: ReadonlyArray<[IContextKey, LanguageFeatureRegistry]> = [ [EditorContextKeys.hasDefinitionProvider.bindTo(contextKeyService), languageFeaturesService.definitionProvider], @@ -161,6 +163,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) 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[] = []; @@ -1016,6 +1018,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 45d04fac45148..959efd746c9ff 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 843d9ed0a0c64..02ea26b67655d 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'; @@ -1029,9 +1029,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) }; }); From d7c3813862057476a87bd5528234df18600710a9 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:35:40 +0000 Subject: [PATCH 32/37] Fix readonly type error in icon label options --- src/vs/workbench/browser/labels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 84b8d6acf893f..8f59f216263ff 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -571,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, From 6cf396e04140edf78b0cfaa64c2cf6793e6dd727 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:46:39 +0000 Subject: [PATCH 33/37] Wire up chat widget labels --- .../chat/browser/chatContentParts/chatMarkdownContentPart.ts | 1 + src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index dc30ab5baa032..41ea6e90b8871 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -340,6 +340,7 @@ class CollapsedCodeBlock extends Disposable { const iconEl = dom.$('span.icon'); iconEl.classList.add(...iconClasses); + Object.assign(iconEl.dataset, iconAttributes); this.element.replaceChildren(iconEl, dom.$('span.icon-label', {}, iconText)); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index c3da0977fac49..2b2d8b3a636c5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -191,6 +191,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}` : ''; From bb0c7f20558630728f39892f454654ced1700c77 Mon Sep 17 00:00:00 2001 From: ZM <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 15 Nov 2024 19:07:34 +0000 Subject: [PATCH 34/37] Fix pushGlobSelectors compile error --- src/vs/workbench/services/themes/browser/fileIconThemeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index c3344a8fb197a..b9f7a256405d8 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -507,7 +507,7 @@ export class FileIconThemeLoader { } } -function pushGlobSelectors(name: string, selectors: string[], kind: string): void { +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'))}`); From 553abf79275b76f5f43840ca2b156d2b283615c5 Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Fri, 15 Nov 2024 19:57:32 +0000 Subject: [PATCH 35/37] Escape handleParentFolder CSS string --- src/vs/workbench/services/themes/browser/fileIconThemeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index b9f7a256405d8..a37655c791cbd 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -538,7 +538,7 @@ function handleParentFolder(key: string, selectors: css.Builder, kind: string): const lastIndexOfSlash = key.lastIndexOf('/'); if (lastIndexOfSlash >= 0) { const parentFolder = key.substring(0, lastIndexOfSlash); - selectors.push(css.inline`.${classSelectorPart(parentFolder)}-dirname-${kind}-icon`); + selectors.push(css.inline`.${classSelectorPart(parentFolder)}-dirname-${classSelectorPart(kind)}-icon`); return key.substring(lastIndexOfSlash + 1); } return key; From a2ef413ed994638be921e067196f02c6af0f42ef Mon Sep 17 00:00:00 2001 From: zm-cttae <114668551+zm-cttae@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:15:30 +0000 Subject: [PATCH 36/37] Get CI passing --- .../chat/browser/chatContentParts/chatMarkdownContentPart.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 916d3d294bd71..537d801a73796 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -343,6 +343,7 @@ class CollapsedCodeBlock extends Disposable { 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)]; From d130c6f5eb2bbbfaba9c04be11bb3eb564d354e6 Mon Sep 17 00:00:00 2001 From: ZM <114668551+zm-cttae@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:22:20 +0000 Subject: [PATCH 37/37] [nit] Fix indent in chatInlineAnchorWidget.ts --- src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index a495917bdc3a2..5be6415725d6a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -111,7 +111,7 @@ 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))]; + iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))]; iconAttributes = getIconAttributes(undefined); this._register(dom.addDisposableListener(element, 'click', () => {