Skip to content

Commit 996bb01

Browse files
author
Akos Kitta
committed
fix: can track and unregister obsolete VSIX themes
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 2faa56e commit 996bb01

File tree

4 files changed

+216
-74
lines changed

4 files changed

+216
-74
lines changed

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ import {
238238
UploadFirmwareDialog,
239239
UploadFirmwareDialogProps,
240240
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
241-
242241
import { UploadCertificate } from './contributions/upload-certificate';
243242
import {
244243
ArduinoFirmwareUploader,
@@ -332,8 +331,11 @@ import { ThemeServiceWithDB } from './theia/core/theming';
332331
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
333332
import {
334333
MonacoThemingService,
335-
ObsoleteThemesCleanup,
334+
CleanupObsoleteIndexedDBThemes,
335+
ThemesRegistrationSummary,
336+
MonacoThemeRegistry,
336337
} from './theia/monaco/monaco-theming-service';
338+
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
337339
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
338340
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
339341
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
@@ -991,8 +993,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
991993

992994
// workaround for themes cannot be removed after registration
993995
// https://github.com/eclipse-theia/theia/issues/11151
994-
bind(ObsoleteThemesCleanup).toSelf().inSingletonScope();
995-
bind(FrontendApplicationContribution).toService(ObsoleteThemesCleanup);
996+
bind(CleanupObsoleteIndexedDBThemes).toSelf().inSingletonScope();
997+
bind(FrontendApplicationContribution).toService(
998+
CleanupObsoleteIndexedDBThemes
999+
);
1000+
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
1001+
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
1002+
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
9961003

9971004
// disable type-hierarchy support
9981005
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be

arduino-ide-extension/src/browser/theia/core/theming.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export namespace ArduinoThemes {
2222
};
2323
}
2424

25-
export const builtInThemeIds = new Set(
25+
const builtInThemeIds = new Set(
2626
[
2727
ArduinoThemes.Light,
2828
ArduinoThemes.Dark,
@@ -35,6 +35,23 @@ export function isBuiltInTheme(theme: Theme | string): boolean {
3535
return builtInThemeIds.has(themeId);
3636
}
3737

38+
export function compatibleBuiltInTheme(theme: Theme): Theme {
39+
switch (theme.type) {
40+
case 'light':
41+
return ArduinoThemes.Light;
42+
case 'dark':
43+
return ArduinoThemes.Dark;
44+
case 'hc':
45+
return BuiltinThemeProvider.hcTheme;
46+
default: {
47+
console.warn(
48+
`Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}`
49+
);
50+
return ArduinoThemes.Light;
51+
}
52+
}
53+
}
54+
3855
export const contributedThemeLabel = nls.localize(
3956
'arduino/theme/contributedTheme',
4057
'Contributed Theme'
Lines changed: 182 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,109 @@
1+
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
12
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
2-
import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming';
3-
import { DisposableCollection } from '@theia/core/lib/common/disposable';
4-
import URI from '@theia/core/lib/common/uri';
3+
import { ThemeService } from '@theia/core/lib/browser/theming';
4+
import { CommandService } from '@theia/core/lib/common/command';
5+
import {
6+
Disposable,
7+
DisposableCollection,
8+
} from '@theia/core/lib/common/disposable';
9+
import { MessageService } from '@theia/core/lib/common/message-service';
10+
import { nls } from '@theia/core/lib/common/nls';
11+
import { deepClone } from '@theia/core/lib/common/objects';
512
import { inject, injectable } from '@theia/core/shared/inversify';
613
import {
14+
MonacoThemeState,
715
deleteTheme as deleteThemeFromIndexedDB,
816
getThemes as getThemesFromIndexedDB,
917
} from '@theia/monaco/lib/browser/monaco-indexed-db';
1018
import {
1119
MonacoTheme,
1220
MonacoThemingService as TheiaMonacoThemingService,
1321
} from '@theia/monaco/lib/browser/monaco-theming-service';
22+
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
23+
import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types';
1424
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
15-
import { ArduinoThemes, builtInThemeIds } from '../core/theming';
25+
import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming';
26+
import { WindowServiceExt } from '../core/window-service-ext';
27+
28+
type MonacoThemeRegistrationSource =
29+
/**
30+
* When reading JS/TS contributed theme from a JSON file. Such as the Arduino themes and the ones contributed by Theia.
31+
*/
32+
| 'compiled'
33+
/**
34+
* When reading and registering previous monaco themes from the `indexedDB`.
35+
*/
36+
| 'indexedDB'
37+
/**
38+
* Contributed by VS Code extensions when starting the app and loading the plugins.
39+
*/
40+
| 'vsix';
41+
42+
@injectable()
43+
export class ThemesRegistrationSummary {
44+
private readonly _summary: Record<MonacoThemeRegistrationSource, string[]> = {
45+
compiled: [],
46+
indexedDB: [],
47+
vsix: [],
48+
};
49+
50+
add(source: MonacoThemeRegistrationSource, themeId: string): void {
51+
const themeIds = this._summary[source];
52+
if (!themeIds.includes(themeId)) {
53+
themeIds.push(themeId);
54+
}
55+
}
56+
57+
get summary(): Record<MonacoThemeRegistrationSource, string[]> {
58+
return deepClone(this._summary);
59+
}
60+
}
61+
62+
@injectable()
63+
export class MonacoThemeRegistry extends TheiaMonacoThemeRegistry {
64+
@inject(ThemesRegistrationSummary)
65+
private readonly summary: ThemesRegistrationSummary;
66+
67+
private initializing = false;
68+
69+
override initializeDefaultThemes(): void {
70+
this.initializing = true;
71+
try {
72+
super.initializeDefaultThemes();
73+
} finally {
74+
this.initializing = false;
75+
}
76+
}
77+
78+
override setTheme(name: string, data: ThemeMix): void {
79+
super.setTheme(name, data);
80+
if (this.initializing) {
81+
this.summary.add('compiled', name);
82+
}
83+
}
84+
}
1685

1786
@injectable()
1887
export class MonacoThemingService extends TheiaMonacoThemingService {
19-
private readonly _pluginContributedThemeIds = new Set<string>();
88+
@inject(ThemesRegistrationSummary)
89+
private readonly summary: ThemesRegistrationSummary;
90+
91+
private themeRegistrationSource: MonacoThemeRegistrationSource | undefined;
92+
93+
protected override async restore(): Promise<void> {
94+
// The custom theme registration must happen before restoring the themes.
95+
// Otherwise, theme changes are not picked up.
96+
// https://github.com/arduino/arduino-ide/issues/1251#issuecomment-1436737702
97+
this.registerArduinoThemes();
98+
this.themeRegistrationSource = 'indexedDB';
99+
try {
100+
await super.restore();
101+
} finally {
102+
this.themeRegistrationSource = 'indexedDB';
103+
}
104+
}
20105

21-
protected override restore(): Promise<void> {
106+
private registerArduinoThemes(): void {
22107
const { Light, Dark } = ArduinoThemes;
23108
this.registerParsedTheme({
24109
id: Light.id,
@@ -32,96 +117,124 @@ export class MonacoThemingService extends TheiaMonacoThemingService {
32117
uiTheme: 'vs-dark',
33118
json: require('../../../../src/browser/data/dark.color-theme.json'),
34119
});
35-
// The custom theme registration must happen before restoring the themes.
36-
// Otherwise, theme changes are not picked up.
37-
// https://github.com/arduino/arduino-ide/issues/1251#issuecomment-1436737702
38-
return super.restore();
39120
}
40121

41-
// Customized to populate `_pluginContributedThemeIds`. The rest of the code is vanilla Theia.
122+
protected override doRegisterParsedTheme(
123+
state: MonacoThemeState
124+
): Disposable {
125+
const themeId = state.id;
126+
const source = this.themeRegistrationSource ?? 'compiled';
127+
const disposable = super.doRegisterParsedTheme(state);
128+
this.summary.add(source, themeId);
129+
return disposable;
130+
}
131+
42132
protected override async doRegister(
43133
theme: MonacoTheme,
44134
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45135
pending: { [uri: string]: Promise<any> },
46136
toDispose: DisposableCollection
47137
): Promise<void> {
48138
try {
49-
const includes = {};
50-
const json = await this.loadTheme(
51-
theme.uri,
52-
includes,
53-
pending,
54-
toDispose
55-
);
56-
if (toDispose.disposed) {
57-
return;
58-
}
59-
const label = theme.label || new URI(theme.uri).path.base;
60-
const { id, description, uiTheme } = theme;
61-
toDispose.push(
62-
this.registerParsedTheme({
63-
id,
64-
label,
65-
description,
66-
uiTheme: uiTheme,
67-
json,
68-
includes,
69-
})
70-
);
71-
// This implementation depends on how Theia internally works.
72-
// Collect all theme IDs contributed by VSIXs.
73-
// When all VISXs are loaded, IDE2 checks the indexedDB for registered themes,
74-
// and if there are obsolete ones, deletes them. A theme is obsolete if it was
75-
// in the DB but was not loaded from a VSIX during the startup.
76-
// See https://github.com/eclipse-theia/theia/issues/11151.
77-
if (new URI(theme.uri).scheme === 'file') {
78-
this._pluginContributedThemeIds.add(theme.id ?? label);
79-
}
80-
} catch (e) {
81-
console.error('Failed to load theme from ' + theme.uri, e);
139+
this.themeRegistrationSource = 'vsix';
140+
await super.doRegister(theme, pending, toDispose);
141+
} finally {
142+
this.themeRegistrationSource = undefined;
82143
}
83144
}
84-
85-
get pluginContributedThemeIds(): string[] {
86-
return Array.from(this._pluginContributedThemeIds.values());
87-
}
88145
}
89146

90-
const compiledThemeIds = new Set([
91-
...builtInThemeIds.values(),
92-
BuiltinThemeProvider.lightTheme,
93-
BuiltinThemeProvider.darkTheme,
94-
]);
95-
96147
/**
97-
* Workaround for removing VSIX themes from the indexedDB if they do not load during app startup.
98-
* This logic cannot be in MonacoThemingService due to a cycle in the DI object graph.
148+
* Workaround for removing VSIX themes from the indexedDB if they were not loaded during the app startup.
99149
*/
100150
@injectable()
101-
export class ObsoleteThemesCleanup implements FrontendApplicationContribution {
151+
export class CleanupObsoleteIndexedDBThemes
152+
implements FrontendApplicationContribution
153+
{
102154
@inject(HostedPluginSupport)
103155
private readonly hostedPlugin: HostedPluginSupport;
104-
@inject(MonacoThemingService)
105-
private readonly monacoTheme: MonacoThemingService;
156+
@inject(ThemesRegistrationSummary)
157+
private readonly summary: ThemesRegistrationSummary;
158+
@inject(ThemeService)
159+
private readonly themeService: ThemeService;
160+
@inject(MessageService)
161+
private readonly messageService: MessageService;
162+
@inject(CommandService)
163+
private readonly commandService: CommandService;
164+
@inject(WindowServiceExt)
165+
private readonly windowService: WindowServiceExt;
106166

107167
onStart(): void {
108-
this.hostedPlugin.didStart.then(() => this.cleanObsoleteThemes());
168+
this.hostedPlugin.didStart.then(() => this.cleanupObsoleteThemes());
109169
}
110170

111-
private async cleanObsoleteThemes(): Promise<void> {
112-
const pluginContributedThemes = this.monacoTheme.pluginContributedThemeIds;
171+
private async cleanupObsoleteThemes(): Promise<void> {
113172
const persistedThemes = await getThemesFromIndexedDB();
114-
// if neither registered by code (e.g. webpack load such as the Arduino Theme) or VSIX, remove from the indexedDB.
115-
const obsoleteThemes = persistedThemes.filter(
116-
({ id }) =>
117-
!pluginContributedThemes.includes(id) && !compiledThemeIds.has(id)
173+
const obsoleteThemeIds = collectObsoleteThemeIds(
174+
persistedThemes,
175+
this.summary.summary
118176
);
119-
if (!obsoleteThemes.length) {
177+
if (!obsoleteThemeIds.length) {
120178
return;
121179
}
122-
await obsoleteThemes.reduce(async (previousTask, theme) => {
180+
await obsoleteThemeIds.reduce(async (previousTask, themeId) => {
123181
await previousTask;
124-
return deleteThemeFromIndexedDB(theme.id);
182+
return deleteThemeFromIndexedDB(themeId);
125183
}, Promise.resolve());
184+
185+
const firstWindow = await this.windowService.isFirstWindow();
186+
if (firstWindow) {
187+
return this.switchToCompatibleBuiltInTheme(obsoleteThemeIds);
188+
}
189+
}
190+
191+
private async switchToCompatibleBuiltInTheme(
192+
obsoleteThemeIds: string[]
193+
): Promise<void> {
194+
const currentTheme = this.themeService.getCurrentTheme();
195+
if (obsoleteThemeIds.includes(currentTheme.id)) {
196+
const message = nls.localize(
197+
'arduino/theme/couldNotLoadTheme',
198+
'Could not load your currently selected theme: {0}. Do you want to automatically select a compatible theme?',
199+
currentTheme.label
200+
);
201+
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
202+
const selectManually = nls.localize(
203+
'arduino/theme/selectManually',
204+
'Select Manually'
205+
);
206+
const answer = await this.messageService.info(
207+
message,
208+
selectManually,
209+
yes
210+
);
211+
if (answer === yes) {
212+
this.themeService.setCurrentTheme(
213+
compatibleBuiltInTheme(currentTheme).id,
214+
true
215+
);
216+
} else if (answer === selectManually) {
217+
return this.commandService.executeCommand(
218+
CommonCommands.SELECT_COLOR_THEME.id
219+
);
220+
}
221+
}
126222
}
127223
}
224+
225+
/**
226+
* An indexedDB registered theme is obsolete if it is in the indexedDB but was registered
227+
* from neither a `vsix` nor `compiled-json` source during the app startup.
228+
*/
229+
export function collectObsoleteThemeIds(
230+
indexedDBThemes: MonacoThemeState[],
231+
summary: Record<MonacoThemeRegistrationSource, string[]>
232+
): string[] {
233+
const vsixThemeIds = summary['vsix'];
234+
const compiledThemeIds = summary['compiled'];
235+
return indexedDBThemes
236+
.map(({ id }) => id)
237+
.filter(
238+
(id) => !vsixThemeIds.includes(id) && !compiledThemeIds.includes(id)
239+
);
240+
}

i18n/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@
466466
"dismissSurvey": "Don't show again",
467467
"surveyMessage": "Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better."
468468
},
469+
"theme": {
470+
"contributedTheme": "Contributed Theme",
471+
"couldNotLoadTheme": "Could not load your currently selected theme: {0}. Do you want to automatically select a compatible theme?",
472+
"selectManually": "Select Manually"
473+
},
469474
"title": {
470475
"cloud": "Cloud"
471476
},

0 commit comments

Comments
 (0)