import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { Disposable, DisposableCollection, } from '@theia/core/lib/common/disposable'; import { MessageService } from '@theia/core/lib/common/message-service'; import { nls } from '@theia/core/lib/common/nls'; import { deepClone } from '@theia/core/lib/common/objects'; import { wait } from '@theia/core/lib/common/promise-util'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MonacoThemeState, deleteTheme as deleteThemeFromIndexedDB, getThemes as getThemesFromIndexedDB, } from '@theia/monaco/lib/browser/monaco-indexed-db'; import { MonacoTheme, MonacoThemingService as TheiaMonacoThemingService, } from '@theia/monaco/lib/browser/monaco-theming-service'; import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry'; import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming'; import { WindowServiceExt } from '../core/window-service-ext'; type MonacoThemeRegistrationSource = /** * When reading JS/TS contributed theme from a JSON file. Such as the Arduino themes and the ones contributed by Theia. */ | 'compiled' /** * When reading and registering previous monaco themes from the `indexedDB`. */ | 'indexedDB' /** * Contributed by VS Code extensions when starting the app and loading the plugins. */ | 'vsix'; @injectable() export class ThemesRegistrationSummary { private readonly _summary: Record = { compiled: [], indexedDB: [], vsix: [], }; add(source: MonacoThemeRegistrationSource, themeId: string): void { const themeIds = this._summary[source]; if (!themeIds.includes(themeId)) { themeIds.push(themeId); } } get summary(): Record { return deepClone(this._summary); } } @injectable() export class MonacoThemeRegistry extends TheiaMonacoThemeRegistry { @inject(ThemesRegistrationSummary) private readonly summary: ThemesRegistrationSummary; private initializing = false; override initializeDefaultThemes(): void { this.initializing = true; try { super.initializeDefaultThemes(); } finally { this.initializing = false; } } override setTheme(name: string, data: ThemeMix): void { super.setTheme(name, data); if (this.initializing) { this.summary.add('compiled', name); } } } @injectable() export class MonacoThemingService extends TheiaMonacoThemingService { @inject(ThemesRegistrationSummary) private readonly summary: ThemesRegistrationSummary; private themeRegistrationSource: MonacoThemeRegistrationSource | undefined; protected override async restore(): Promise { // The custom theme registration must happen before restoring the themes. // Otherwise, theme changes are not picked up. // https://github.com/arduino/arduino-ide/issues/1251#issuecomment-1436737702 this.registerArduinoThemes(); this.themeRegistrationSource = 'indexedDB'; try { await super.restore(); } finally { this.themeRegistrationSource = 'indexedDB'; } } private registerArduinoThemes(): void { const { light, dark } = ArduinoThemes; this.registerParsedTheme({ id: light.id, label: light.label, uiTheme: 'vs', json: require('../../../../src/browser/data/default.color-theme.json'), }); this.registerParsedTheme({ id: dark.id, label: dark.label, uiTheme: 'vs-dark', json: require('../../../../src/browser/data/dark.color-theme.json'), }); } protected override doRegisterParsedTheme( state: MonacoThemeState ): Disposable { const themeId = state.id; const source = this.themeRegistrationSource ?? 'compiled'; const disposable = super.doRegisterParsedTheme(state); this.summary.add(source, themeId); return disposable; } protected override async doRegister( theme: MonacoTheme, // eslint-disable-next-line @typescript-eslint/no-explicit-any pending: { [uri: string]: Promise }, toDispose: DisposableCollection ): Promise { try { this.themeRegistrationSource = 'vsix'; await super.doRegister(theme, pending, toDispose); } finally { this.themeRegistrationSource = undefined; } } } /** * Workaround for removing VSIX themes from the indexedDB if they were not loaded during the app startup. */ @injectable() export class CleanupObsoleteThemes implements FrontendApplicationContribution { @inject(HostedPluginSupport) private readonly hostedPlugin: HostedPluginSupport; @inject(ThemesRegistrationSummary) private readonly summary: ThemesRegistrationSummary; @inject(ThemeService) private readonly themeService: ThemeService; @inject(MessageService) private readonly messageService: MessageService; @inject(WindowServiceExt) private readonly windowService: WindowServiceExt; onStart(): void { this.hostedPlugin.didStart.then(() => this.cleanupObsoleteThemes()); } private async cleanupObsoleteThemes(): Promise { const persistedThemes = await getThemesFromIndexedDB(); const obsoleteThemeIds = collectObsoleteThemeIds( persistedThemes, this.summary.summary ); if (!obsoleteThemeIds.length) { return; } const firstWindow = await this.windowService.isFirstWindow(); if (firstWindow) { await this.removeObsoleteThemesFromIndexedDB(obsoleteThemeIds); this.unregisterObsoleteThemes(obsoleteThemeIds); } } private removeObsoleteThemesFromIndexedDB(themeIds: string[]): Promise { return themeIds.reduce(async (previousTask, themeId) => { await previousTask; return deleteThemeFromIndexedDB(themeId); }, Promise.resolve()); } private unregisterObsoleteThemes(themeIds: string[]): void { const currentTheme = this.themeService.getCurrentTheme(); const switchToCompatibleTheme = themeIds.includes(currentTheme.id); for (const themeId of themeIds) { delete this.themeService['themes'][themeId]; } this.themeService['doUpdateColorThemePreference'](); if (switchToCompatibleTheme) { this.themeService.setCurrentTheme( compatibleBuiltInTheme(currentTheme).id, true ); wait(250).then(() => requestAnimationFrame(() => this.messageService.info( nls.localize( 'arduino/theme/currentThemeNotFound', 'Could not find the currently selected theme: {0}. Arduino IDE has picked a built-in theme compatible with the missing one.', currentTheme.label ) ) ) ); } } } /** * An indexedDB registered theme is obsolete if it is in the indexedDB but was registered * from neither a `vsix` nor `compiled` source during the app startup. */ export function collectObsoleteThemeIds( indexedDBThemes: MonacoThemeState[], summary: Record ): string[] { const vsixThemeIds = summary['vsix']; const compiledThemeIds = summary['compiled']; return indexedDBThemes .map(({ id }) => id) .filter( (id) => !vsixThemeIds.includes(id) && !compiledThemeIds.includes(id) ); }