1
+ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution' ;
1
2
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' ;
5
12
import { inject , injectable } from '@theia/core/shared/inversify' ;
6
13
import {
14
+ MonacoThemeState ,
7
15
deleteTheme as deleteThemeFromIndexedDB ,
8
16
getThemes as getThemesFromIndexedDB ,
9
17
} from '@theia/monaco/lib/browser/monaco-indexed-db' ;
10
18
import {
11
19
MonacoTheme ,
12
20
MonacoThemingService as TheiaMonacoThemingService ,
13
21
} 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' ;
14
24
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
+ }
16
85
17
86
@injectable ( )
18
87
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
+ }
20
105
21
- protected override restore ( ) : Promise < void > {
106
+ private registerArduinoThemes ( ) : void {
22
107
const { Light, Dark } = ArduinoThemes ;
23
108
this . registerParsedTheme ( {
24
109
id : Light . id ,
@@ -32,96 +117,124 @@ export class MonacoThemingService extends TheiaMonacoThemingService {
32
117
uiTheme : 'vs-dark' ,
33
118
json : require ( '../../../../src/browser/data/dark.color-theme.json' ) ,
34
119
} ) ;
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 ( ) ;
39
120
}
40
121
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
+
42
132
protected override async doRegister (
43
133
theme : MonacoTheme ,
44
134
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45
135
pending : { [ uri : string ] : Promise < any > } ,
46
136
toDispose : DisposableCollection
47
137
) : Promise < void > {
48
138
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 ;
82
143
}
83
144
}
84
-
85
- get pluginContributedThemeIds ( ) : string [ ] {
86
- return Array . from ( this . _pluginContributedThemeIds . values ( ) ) ;
87
- }
88
145
}
89
146
90
- const compiledThemeIds = new Set ( [
91
- ...builtInThemeIds . values ( ) ,
92
- BuiltinThemeProvider . lightTheme ,
93
- BuiltinThemeProvider . darkTheme ,
94
- ] ) ;
95
-
96
147
/**
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.
99
149
*/
100
150
@injectable ( )
101
- export class ObsoleteThemesCleanup implements FrontendApplicationContribution {
151
+ export class CleanupObsoleteIndexedDBThemes
152
+ implements FrontendApplicationContribution
153
+ {
102
154
@inject ( HostedPluginSupport )
103
155
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 ;
106
166
107
167
onStart ( ) : void {
108
- this . hostedPlugin . didStart . then ( ( ) => this . cleanObsoleteThemes ( ) ) ;
168
+ this . hostedPlugin . didStart . then ( ( ) => this . cleanupObsoleteThemes ( ) ) ;
109
169
}
110
170
111
- private async cleanObsoleteThemes ( ) : Promise < void > {
112
- const pluginContributedThemes = this . monacoTheme . pluginContributedThemeIds ;
171
+ private async cleanupObsoleteThemes ( ) : Promise < void > {
113
172
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
118
176
) ;
119
- if ( ! obsoleteThemes . length ) {
177
+ if ( ! obsoleteThemeIds . length ) {
120
178
return ;
121
179
}
122
- await obsoleteThemes . reduce ( async ( previousTask , theme ) => {
180
+ await obsoleteThemeIds . reduce ( async ( previousTask , themeId ) => {
123
181
await previousTask ;
124
- return deleteThemeFromIndexedDB ( theme . id ) ;
182
+ return deleteThemeFromIndexedDB ( themeId ) ;
125
183
} , 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
+ }
126
222
}
127
223
}
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
+ }
0 commit comments