From ec4e4037311fd5df58230caaefc470253d217f58 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Thu, 19 Oct 2023 15:42:42 +0200 Subject: [PATCH 1/2] Support language-specific file icons from plugins - Extend language service with necessary methods - Create CSS rules for contributed language icons - Use language icons if theme sets 'showLanguageModeIcons' - Ensure custom theme can override language icons --- .../browser/frontend-application-module.ts | 3 + .../src/browser/icon-theme-contribution.ts | 1 + .../core/src/browser/icon-theme-service.ts | 1 + .../src/browser/language-icon-provider.ts | 36 ++++++++++++ packages/core/src/browser/language-service.ts | 23 ++++++++ .../monaco/src/browser/monaco-languages.ts | 53 +++++++++++++++++ .../plugin-ext/src/common/plugin-protocol.ts | 5 ++ .../src/hosted/node/scanners/scanner-theia.ts | 14 +++-- .../browser/plugin-contribution-handler.ts | 9 +++ .../main/browser/plugin-icon-theme-service.ts | 58 +++++++++++++++---- .../src/main/browser/plugin-shared-style.ts | 53 +++++++++++------ 11 files changed, 221 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/browser/language-icon-provider.ts diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index dbfc14fef85f1..45e83b311720e 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -138,6 +138,7 @@ import { StylingParticipant, StylingService } from './styling-service'; import { bindCommonStylingParticipants } from './common-styling-participants'; import { HoverService } from './hover-service'; import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; +import { LanguageIconLabelProvider } from './language-icon-provider'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -150,6 +151,8 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(IconThemeContribution).toService(DefaultFileIconThemeContribution); bind(IconThemeApplicationContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(IconThemeApplicationContribution); + bind(LanguageIconLabelProvider).toSelf().inSingletonScope(); + bind(LabelProviderContribution).toService(LanguageIconLabelProvider); bind(ColorRegistry).toSelf().inSingletonScope(); bindContributionProvider(bind, ColorContribution); diff --git a/packages/core/src/browser/icon-theme-contribution.ts b/packages/core/src/browser/icon-theme-contribution.ts index b192588a69547..f4b27a4fdfc07 100644 --- a/packages/core/src/browser/icon-theme-contribution.ts +++ b/packages/core/src/browser/icon-theme-contribution.ts @@ -50,6 +50,7 @@ export class DefaultFileIconThemeContribution implements IconTheme, IconThemeCon readonly label = 'File Icons (Theia)'; readonly hasFileIcons = true; readonly hasFolderIcons = true; + readonly showLanguageModeIcons = true; registerIconThemes(iconThemes: IconThemeService): MaybePromise { iconThemes.register(this); diff --git a/packages/core/src/browser/icon-theme-service.ts b/packages/core/src/browser/icon-theme-service.ts index 2df1efdcf4806..ecf237d2b31b8 100644 --- a/packages/core/src/browser/icon-theme-service.ts +++ b/packages/core/src/browser/icon-theme-service.ts @@ -31,6 +31,7 @@ export interface IconThemeDefinition { readonly hasFileIcons?: boolean; readonly hasFolderIcons?: boolean; readonly hidesExplorerArrows?: boolean; + readonly showLanguageModeIcons?: boolean; } export interface IconTheme extends IconThemeDefinition { diff --git a/packages/core/src/browser/language-icon-provider.ts b/packages/core/src/browser/language-icon-provider.ts new file mode 100644 index 0000000000000..32500e08abe74 --- /dev/null +++ b/packages/core/src/browser/language-icon-provider.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from 'inversify'; +import { IconThemeService } from './icon-theme-service'; +import { LabelProviderContribution } from './label-provider'; +import { LanguageService } from './language-service'; + +@injectable() +export class LanguageIconLabelProvider implements LabelProviderContribution { + @inject(IconThemeService) protected readonly iconThemeService: IconThemeService; + @inject(LanguageService) protected readonly languageService: LanguageService; + + canHandle(element: object): number { + const current = this.iconThemeService.getDefinition(this.iconThemeService.current); + return current?.showLanguageModeIcons === true && this.languageService.getIcon(element) ? Number.MAX_SAFE_INTEGER : 0; + } + + getIcon(element: object): string | undefined { + const language = this.languageService.detectLanguage(element); + return this.languageService.getIcon(language!.id); + } +} diff --git a/packages/core/src/browser/language-service.ts b/packages/core/src/browser/language-service.ts index 5897f779e4610..c00989a6b2054 100644 --- a/packages/core/src/browser/language-service.ts +++ b/packages/core/src/browser/language-service.ts @@ -15,12 +15,14 @@ // ***************************************************************************** import { injectable } from 'inversify'; +import { Disposable } from '../common'; export interface Language { readonly id: string; readonly name: string; readonly extensions: Set; readonly filenames: Set; + readonly iconClass?: string; } @injectable() @@ -40,4 +42,25 @@ export class LanguageService { return undefined; } + /** + * It should be implemented by an extension, e.g. by the monaco extension. + */ + detectLanguage(obj: unknown): Language | undefined { + return undefined; + } + + /** + * It should be implemented by an extension, e.g. by the monaco extension. + */ + registerIcon(languageId: string, iconClass: string): Disposable { + return Disposable.NULL; + } + + /** + * It should be implemented by an extension, e.g. by the monaco extension. + */ + getIcon(obj: unknown): string | undefined { + return undefined; + } + } diff --git a/packages/monaco/src/browser/monaco-languages.ts b/packages/monaco/src/browser/monaco-languages.ts index d533475d804e0..bbefedc219548 100644 --- a/packages/monaco/src/browser/monaco-languages.ts +++ b/packages/monaco/src/browser/monaco-languages.ts @@ -25,6 +25,10 @@ import { Language, LanguageService } from '@theia/core/lib/browser/language-serv import { MonacoMarkerCollection } from './monaco-marker-collection'; import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter'; import * as monaco from '@theia/monaco-editor-core'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { FileStatNode } from '@theia/filesystem/lib/browser'; +import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; export interface WorkspaceSymbolProvider { provideWorkspaceSymbols(params: WorkspaceSymbolParams, token: CancellationToken): MaybePromise; @@ -37,6 +41,7 @@ export class MonacoLanguages implements LanguageService { readonly workspaceSymbolProviders: WorkspaceSymbolProvider[] = []; protected readonly markers = new Map(); + protected readonly icons = new Map(); @inject(ProblemManager) protected readonly problemManager: ProblemManager; @inject(ProtocolToMonacoConverter) protected readonly p2m: ProtocolToMonacoConverter; @@ -81,10 +86,58 @@ export class MonacoLanguages implements LanguageService { return this.mergeLanguages(monaco.languages.getLanguages().filter(language => language.id === languageId)).get(languageId); } + detectLanguage(obj: unknown): Language | undefined { + if (obj === undefined) { + return undefined; + } + if (typeof obj === 'string') { + return this.detectLanguageByIdOrName(obj) ?? this.detectLanguageByURI(new URI(obj)); + } + if (obj instanceof URI) { + return this.detectLanguageByURI(obj); + } + if (FileStat.is(obj)) { + return this.detectLanguageByURI(obj.resource); + } + if (FileStatNode.is(obj)) { + return this.detectLanguageByURI(obj.uri); + } + return undefined; + } + + protected detectLanguageByIdOrName(obj: string): Language | undefined { + const languageById = this.getLanguage(obj); + if (languageById) { + return languageById; + } + + const languageId = this.getLanguageIdByLanguageName(obj); + return languageId ? this.getLanguage(languageId) : undefined; + } + + protected detectLanguageByURI(uri: URI): Language | undefined { + const languageId = StandaloneServices.get(ILanguageService).guessLanguageIdByFilepathOrFirstLine(uri['codeUri']); + return languageId ? this.getLanguage(languageId) : undefined; + } + getExtension(languageId: string): string | undefined { return this.getLanguage(languageId)?.extensions.values().next().value; } + registerIcon(languageId: string, iconClass: string): Disposable { + this.icons.set(languageId, iconClass); + return Disposable.create(() => this.icons.delete(languageId)); + } + + getIcon(obj: unknown): string | undefined { + const language = this.detectLanguage(obj); + return language ? this.icons.get(language.id) : undefined; + } + + getLanguageIdByLanguageName(languageName: string): string | undefined { + return monaco.languages.getLanguages().find(language => language.aliases?.includes(languageName))?.id; + } + protected mergeLanguages(registered: monaco.languages.ILanguageExtensionPoint[]): Map> { const languages = new Map>(); for (const { id, aliases, extensions, filenames } of registered) { diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 88a73813995af..c43545676b26b 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -308,6 +308,7 @@ export interface PluginPackageLanguageContribution { aliases?: string[]; mimetypes?: string[]; configuration?: string; + icon?: IconUrl; } export interface PluginPackageLanguageContributionConfiguration { @@ -715,6 +716,10 @@ export interface LanguageContribution { aliases?: string[]; mimetypes?: string[]; configuration?: LanguageConfiguration; + /** + * @internal + */ + icon?: IconUrl; } export interface RegExpOptions { diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 084356e465de4..559f08aa18eaa 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -371,7 +371,7 @@ export class TheiaPluginScanner implements PluginScanner { } const [languagesResult, grammarsResult] = await Promise.allSettled([ - rawPlugin.contributes.languages ? this.readLanguages(rawPlugin.contributes.languages, rawPlugin.packagePath) : undefined, + rawPlugin.contributes.languages ? this.readLanguages(rawPlugin.contributes.languages, rawPlugin) : undefined, rawPlugin.contributes.grammars ? this.grammarsReader.readGrammars(rawPlugin.contributes.grammars, rawPlugin.packagePath) : undefined ]); @@ -723,8 +723,8 @@ export class TheiaPluginScanner implements PluginScanner { return result; } - private async readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): Promise { - return Promise.all(rawLanguages.map(language => this.readLanguage(language, pluginPath))); + private async readLanguages(rawLanguages: PluginPackageLanguageContribution[], plugin: PluginPackage): Promise { + return Promise.all(rawLanguages.map(language => this.readLanguage(language, plugin))); } private readSubmenus(rawSubmenus: PluginPackageSubmenu[], plugin: PluginPackage): Submenu[] { @@ -741,8 +741,9 @@ export class TheiaPluginScanner implements PluginScanner { } - private async readLanguage(rawLang: PluginPackageLanguageContribution, pluginPath: string): Promise { + private async readLanguage(rawLang: PluginPackageLanguageContribution, plugin: PluginPackage): Promise { // TODO: add validation to all parameters + const icon = this.transformIconUrl(plugin, rawLang.icon); const result: LanguageContribution = { id: rawLang.id, aliases: rawLang.aliases, @@ -750,10 +751,11 @@ export class TheiaPluginScanner implements PluginScanner { filenamePatterns: rawLang.filenamePatterns, filenames: rawLang.filenames, firstLine: rawLang.firstLine, - mimetypes: rawLang.mimetypes + mimetypes: rawLang.mimetypes, + icon: icon?.iconUrl ?? icon?.themeIcon }; if (rawLang.configuration) { - const rawConfiguration = await this.readJson(path.resolve(pluginPath, rawLang.configuration)); + const rawConfiguration = await this.readJson(path.resolve(plugin.packagePath, rawLang.configuration)); if (rawConfiguration) { const configuration: LanguageConfiguration = { brackets: rawConfiguration.brackets, diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index d041566fb8e20..edd4584b67d73 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -53,6 +53,7 @@ import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { PluginTerminalRegistry } from './plugin-terminal-registry'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; @injectable() export class PluginContributionHandler { @@ -89,6 +90,9 @@ export class PluginContributionHandler { @inject(CommandRegistry) protected readonly commands: CommandRegistry; + @inject(LanguageService) + protected readonly languageService: LanguageService; + @inject(PluginSharedStyle) protected readonly style: PluginSharedStyle; @@ -195,6 +199,11 @@ export class PluginContributionHandler { firstLine: lang.firstLine, mimetypes: lang.mimetypes }); + if (lang.icon) { + const languageIcon = this.style.toFileIconClass(lang.icon); + pushContribution(`language.${lang.id}.icon`, () => languageIcon); + pushContribution(`language.${lang.id}.iconRegistration`, () => this.languageService.registerIcon(lang.id, languageIcon.object.iconClass)); + } const langConfiguration = lang.configuration; if (langConfiguration) { pushContribution(`language.${lang.id}.configuration`, () => monaco.languages.setLanguageConfiguration(lang.id, { diff --git a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts index 7f364328b15ab..b58625766ff45 100644 --- a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts +++ b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts @@ -39,6 +39,8 @@ import { FileStat, FileChangeType } from '@theia/filesystem/lib/common/files'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; +import { DEFAULT_ICON_SIZE, PLUGIN_FILE_ICON_CLASS } from './plugin-shared-style'; export interface PluginIconDefinition { iconPath: string; @@ -79,6 +81,7 @@ export interface PluginIconThemeDocument extends PluginIconsAssociation { light?: PluginIconsAssociation; highContrast?: PluginIconsAssociation; hidesExplorerArrows?: boolean; + showLanguageModeIcons?: boolean; } export const PluginIconThemeFactory = Symbol('PluginIconThemeFactory'); @@ -96,8 +99,14 @@ export class PluginIconThemeDefinition implements IconThemeDefinition, IconTheme hasFileIcons?: boolean; hasFolderIcons?: boolean; hidesExplorerArrows?: boolean; + showLanguageModeIcons?: boolean; } +class PluginLanguageIconInfo { + hasSpecificFileIcons: boolean = false; + coveredLanguages: { [languageId: string]: boolean } = {}; +}; + @injectable() export class PluginIconTheme extends PluginIconThemeDefinition implements IconTheme, Disposable { @@ -113,6 +122,9 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(LanguageService) + protected readonly languageService: LanguageService; + protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange = this.onDidChangeEmitter.event; @@ -240,17 +252,18 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh selectors.push(selector + '::before'); definitionSelectors.set(definitionId, selectors); }; - this.collectSelectors(json, acceptSelector.bind(undefined, 'dark')); + + let iconInfo = this.collectSelectors(json, acceptSelector.bind(undefined, 'dark')); if (json.light) { - this.collectSelectors(json.light, acceptSelector.bind(undefined, 'light')); + iconInfo = this.collectSelectors(json.light, acceptSelector.bind(undefined, 'light')); } if (json.highContrast) { - this.collectSelectors(json.highContrast, acceptSelector.bind(undefined, 'hc')); + iconInfo = this.collectSelectors(json.highContrast, acceptSelector.bind(undefined, 'hc')); } - if (!this.icons.size) { - return; - } + const showLanguageModeIcons = this.showLanguageModeIcons === true + || json.showLanguageModeIcons === true + || (iconInfo.hasSpecificFileIcons && json.showLanguageModeIcons !== false); const fonts = json.fonts; if (Array.isArray(fonts)) { @@ -303,7 +316,7 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh this.styleSheetContent += `${selectors.join(', ')} { content: ' '; background-image: ${cssUrl}; - background-size: 16px; + background-size: ${DEFAULT_ICON_SIZE}px; background-position: left center; background-repeat: no-repeat; } @@ -327,6 +340,20 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh } } } + + if (showLanguageModeIcons) { + for (const language of this.languageService.languages) { + // only show language icons if there are no more specific icons in the style document + if (!iconInfo.coveredLanguages[language.id]) { + const icon = this.languageService.getIcon(language.id); + if (icon) { + this.icons.add(this.fileIcon); + this.icons.add(this.languageIcon(language.id)); + this.icons.add(icon); + } + } + } + } } protected toCSSUrl(iconPath: string | undefined): string | undefined { @@ -348,7 +375,7 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh return value; } - protected readonly fileIcon = 'theia-plugin-file-icon'; + protected readonly fileIcon = PLUGIN_FILE_ICON_CLASS; protected readonly folderIcon = 'theia-plugin-folder-icon'; protected readonly folderExpandedIcon = 'theia-plugin-folder-expanded-icon'; protected readonly rootFolderIcon = 'theia-plugin-root-folder-icon'; @@ -384,10 +411,8 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh return 'theia-plugin-' + this.escapeCSS(languageId) + '-lang-file-icon'; } - protected collectSelectors( - associations: RecursivePartial, - accept: (definitionId: string, ...icons: string[]) => void - ): void { + protected collectSelectors(associations: RecursivePartial, accept: (definitionId: string, ...icons: string[]) => void): PluginLanguageIconInfo { + const iconInfo = new PluginLanguageIconInfo(); if (associations.folder) { accept(associations.folder, this.folderIcon); if (associations.folderExpanded === undefined) { @@ -441,6 +466,8 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh for (const languageId in languageIds) { accept(languageIds[languageId]!, this.languageIcon(languageId), this.fileIcon); this.hasFileIcons = true; + iconInfo.hasSpecificFileIcons = true; + iconInfo.coveredLanguages[languageId] = true; } } const fileExtensions = associations.fileExtensions; @@ -449,6 +476,7 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh for (const fileExtension in fileExtensions) { accept(fileExtensions[fileExtension]!, ...this.fileExtensionIcon(fileExtension), this.fileIcon); this.hasFileIcons = true; + iconInfo.hasSpecificFileIcons = true; } } const fileNames = associations.fileNames; @@ -457,8 +485,10 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh for (const fileName in fileNames) { accept(fileNames[fileName]!, ...this.fileNameIcon(fileName), this.fileIcon); this.hasFileIcons = true; + iconInfo.hasSpecificFileIcons = true; } } + return iconInfo; } /** @@ -529,6 +559,10 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh } const language = StandaloneServices.get(ILanguageService).createByFilepathOrFirstLine(parsedURI['codeUri']); classNames.push(this.languageIcon(language.languageId)); + const defaultLanguageIcon = this.languageService.getIcon(language.languageId); + if (defaultLanguageIcon) { + classNames.push(defaultLanguageIcon); + } } return classNames; } diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts index 72a708b261ef7..8fa32a52d9fba 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -23,14 +23,19 @@ import { Reference, SyncReferenceCollection } from '@theia/core/lib/common/refer import { Endpoint } from '@theia/core/lib/browser/endpoint'; export interface PluginIconKey { - url: IconUrl - size: number + url: IconUrl; + size?: number; + type?: 'icon' | 'file'; } export interface PluginIcon extends Disposable { readonly iconClass: string } +export const PLUGIN_FILE_ICON_CLASS = 'theia-plugin-file-icon'; + +export const DEFAULT_ICON_SIZE = 16; + @injectable() export class PluginSharedStyle { @@ -98,30 +103,44 @@ export class PluginSharedStyle { } private readonly icons = new SyncReferenceCollection(key => this.createPluginIcon(key)); - toIconClass(url: IconUrl, { size }: { size: number } = { size: 16 }): Reference { + toIconClass(url: IconUrl, { size }: { size: number } = { size: DEFAULT_ICON_SIZE }): Reference { return this.icons.acquire({ url, size }); } + toFileIconClass(url: IconUrl): Reference { + return this.icons.acquire({ url, type: 'file' }); + } + private iconSequence = 0; protected createPluginIcon(key: PluginIconKey): PluginIcon { const iconUrl = key.url; - const size = key.size; + const size = key.size ?? DEFAULT_ICON_SIZE; + const type = key.type ?? 'icon'; const darkIconUrl = PluginSharedStyle.toExternalIconUrl(`${typeof iconUrl === 'object' ? iconUrl.dark : iconUrl}`); const lightIconUrl = PluginSharedStyle.toExternalIconUrl(`${typeof iconUrl === 'object' ? iconUrl.light : iconUrl}`); - const iconClass = 'plugin-icon-' + this.iconSequence++; + const toDispose = new DisposableCollection(); - toDispose.push(this.insertRule('.' + iconClass + '::before', theme => ` - content: ""; - background-position: 2px; - width: ${size}px; - height: ${size}px; - background: center no-repeat url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}"); - background-size: ${size}px; - `)); - return { - iconClass, - dispose: () => toDispose.dispose() - }; + let iconClass = 'plugin-icon-' + this.iconSequence++; + if (type === 'icon') { + toDispose.push(this.insertRule('.' + iconClass + '::before', theme => ` + content: ""; + background-position: 2px; + width: ${size}'px'; + height: ${size}'px'; + background: center no-repeat url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}"); + background-size: ${size}px; + `)); + } else { + toDispose.push(this.insertRule('.' + iconClass + '::before', theme => ` + content: ""; + background-image: url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}"); + background-size: ${DEFAULT_ICON_SIZE}px; + background-position: left center; + background-repeat: no-repeat; + `)); + iconClass += ' ' + PLUGIN_FILE_ICON_CLASS; + } + return { iconClass, dispose: () => toDispose.dispose() }; } static toExternalIconUrl(iconUrl: string): string { From 5602b6ade3208121a5b2ba50fb0c4d501059a9fd Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 8 Nov 2023 09:25:57 +0100 Subject: [PATCH 2/2] Review: Properly emit update when icon is changed --- .../src/browser/language-icon-provider.ts | 23 +++++++++++++++++-- packages/core/src/browser/language-service.ts | 13 ++++++++++- .../monaco/src/browser/monaco-languages.ts | 18 +++++++++------ 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/core/src/browser/language-icon-provider.ts b/packages/core/src/browser/language-icon-provider.ts index 32500e08abe74..371695e247723 100644 --- a/packages/core/src/browser/language-icon-provider.ts +++ b/packages/core/src/browser/language-icon-provider.ts @@ -14,9 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; +import { Emitter, Event } from '../common'; import { IconThemeService } from './icon-theme-service'; -import { LabelProviderContribution } from './label-provider'; +import { DidChangeLabelEvent, LabelProviderContribution } from './label-provider'; import { LanguageService } from './language-service'; @injectable() @@ -24,6 +25,13 @@ export class LanguageIconLabelProvider implements LabelProviderContribution { @inject(IconThemeService) protected readonly iconThemeService: IconThemeService; @inject(LanguageService) protected readonly languageService: LanguageService; + protected readonly onDidChangeEmitter = new Emitter(); + + @postConstruct() + protected init(): void { + this.languageService.onDidChangeIcon(() => this.fireDidChange()); + } + canHandle(element: object): number { const current = this.iconThemeService.getDefinition(this.iconThemeService.current); return current?.showLanguageModeIcons === true && this.languageService.getIcon(element) ? Number.MAX_SAFE_INTEGER : 0; @@ -33,4 +41,15 @@ export class LanguageIconLabelProvider implements LabelProviderContribution { const language = this.languageService.detectLanguage(element); return this.languageService.getIcon(language!.id); } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } + + protected fireDidChange(): void { + this.onDidChangeEmitter.fire({ + affects: element => this.canHandle(element) > 0 + }); + } + } diff --git a/packages/core/src/browser/language-service.ts b/packages/core/src/browser/language-service.ts index c00989a6b2054..15d36faa7a423 100644 --- a/packages/core/src/browser/language-service.ts +++ b/packages/core/src/browser/language-service.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable } from 'inversify'; -import { Disposable } from '../common'; +import { Disposable, Emitter, Event } from '../common'; export interface Language { readonly id: string; @@ -27,6 +27,7 @@ export interface Language { @injectable() export class LanguageService { + protected readonly onDidChangeIconEmitter = new Emitter(); /** * It should be implemented by an extension, e.g. by the monaco extension. @@ -63,4 +64,14 @@ export class LanguageService { return undefined; } + /** + * Emit when the icon of a particular language was changed. + */ + get onDidChangeIcon(): Event { + return this.onDidChangeIconEmitter.event; + } +} + +export interface DidChangeIconEvent { + languageId: string; } diff --git a/packages/monaco/src/browser/monaco-languages.ts b/packages/monaco/src/browser/monaco-languages.ts index bbefedc219548..fde92eec8db5c 100644 --- a/packages/monaco/src/browser/monaco-languages.ts +++ b/packages/monaco/src/browser/monaco-languages.ts @@ -36,7 +36,7 @@ export interface WorkspaceSymbolProvider { } @injectable() -export class MonacoLanguages implements LanguageService { +export class MonacoLanguages extends LanguageService { readonly workspaceSymbolProviders: WorkspaceSymbolProvider[] = []; @@ -78,15 +78,15 @@ export class MonacoLanguages implements LanguageService { }); } - get languages(): Language[] { + override get languages(): Language[] { return [...this.mergeLanguages(monaco.languages.getLanguages()).values()]; } - getLanguage(languageId: string): Language | undefined { + override getLanguage(languageId: string): Language | undefined { return this.mergeLanguages(monaco.languages.getLanguages().filter(language => language.id === languageId)).get(languageId); } - detectLanguage(obj: unknown): Language | undefined { + override detectLanguage(obj: unknown): Language | undefined { if (obj === undefined) { return undefined; } @@ -124,12 +124,16 @@ export class MonacoLanguages implements LanguageService { return this.getLanguage(languageId)?.extensions.values().next().value; } - registerIcon(languageId: string, iconClass: string): Disposable { + override registerIcon(languageId: string, iconClass: string): Disposable { this.icons.set(languageId, iconClass); - return Disposable.create(() => this.icons.delete(languageId)); + this.onDidChangeIconEmitter.fire({ languageId }); + return Disposable.create(() => { + this.icons.delete(languageId); + this.onDidChangeIconEmitter.fire({ languageId }); + }); } - getIcon(obj: unknown): string | undefined { + override getIcon(obj: unknown): string | undefined { const language = this.detectLanguage(obj); return language ? this.icons.get(language.id) : undefined; }