2025-03-29 01:33:25 +09:00

232 lines
7.4 KiB
TypeScript

import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
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 '../../hosted/hosted-plugin-support';
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<MonacoThemeRegistrationSource, string[]> = {
compiled: [],
indexedDB: [],
vsix: [],
};
add(source: MonacoThemeRegistrationSource, themeId: string): void {
const themeIds = this._summary[source];
if (!themeIds.includes(themeId)) {
themeIds.push(themeId);
}
}
get summary(): Record<MonacoThemeRegistrationSource, string[]> {
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<void> {
// 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<any> },
toDispose: DisposableCollection
): Promise<void> {
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<void> {
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<void> {
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<MonacoThemeRegistrationSource, string[]>
): string[] {
const vsixThemeIds = summary['vsix'];
const compiledThemeIds = summary['compiled'];
return indexedDBThemes
.map(({ id }) => id)
.filter(
(id) => !vsixThemeIds.includes(id) && !compiledThemeIds.includes(id)
);
}