diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
index 09475690b..8a5b4467a 100644
--- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
+++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
@@ -238,7 +238,6 @@ import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
-
import { UploadCertificate } from './contributions/upload-certificate';
import {
ArduinoFirmwareUploader,
@@ -328,9 +327,13 @@ import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
-import { ThemeServiceWithDB } from './theia/core/theming';
-import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
-import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
+import {
+ MonacoThemingService,
+ CleanupObsoleteThemes,
+ ThemesRegistrationSummary,
+ MonacoThemeRegistry,
+} from './theia/monaco/monaco-theming-service';
+import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
@@ -973,11 +976,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
- bind(ThemeServiceWithDB).toSelf().inSingletonScope();
- rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
+ // workaround for themes cannot be removed after registration
+ // https://github.com/eclipse-theia/theia/issues/11151
+ bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
+ bind(FrontendApplicationContribution).toService(
+ CleanupObsoleteThemes
+ );
+ bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
+ bind(MonacoThemeRegistry).toSelf().inSingletonScope();
+ rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
+
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
index a5249326b..644484f01 100644
--- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
+++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx
@@ -24,6 +24,12 @@ import {
} from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input';
import { InterfaceScale } from '../../contributions/interface-scale';
+import {
+ userConfigurableThemes,
+ themeLabelForSettings,
+ arduinoThemeTypeOf,
+} from '../../theia/core/theming';
+import { Theme } from '@theia/core/lib/common/theme';
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MAX
@@ -218,14 +224,10 @@ export class SettingsComponent extends React.Component<
@@ -333,6 +335,46 @@ export class SettingsComponent extends React.Component<
);
}
+ private get currentThemeLabel(): string {
+ const currentTheme = this.props.themeService.getCurrentTheme();
+ return themeLabelForSettings(currentTheme);
+ }
+
+ private get separatedThemes(): (Theme | string)[] {
+ const separatedThemes: (Theme | string)[] = [];
+ const groupedThemes = userConfigurableThemes(this.props.themeService);
+ for (const group of groupedThemes) {
+ for (let i = 0; i < group.length; i++) {
+ const theme = group[i];
+ if (i === 0 && separatedThemes.length) {
+ const arduinoThemeType = arduinoThemeTypeOf(theme);
+ separatedThemes.push(`separator-${arduinoThemeType}`);
+ }
+ separatedThemes.push(theme);
+ }
+ }
+ return separatedThemes;
+ }
+
+ private get themeSelectOptions(): React.ReactNode[] {
+ return this.separatedThemes.map((item) => {
+ if (typeof item === 'string') {
+ return (
+ // ─ -> BOX DRAWINGS LIGHT HORIZONTAL
+
+ );
+ }
+ const label = themeLabelForSettings(item);
+ return (
+
+ );
+ });
+ }
+
private toSelectOptions(language: string | LanguageInfo): JSX.Element {
const plain = typeof language === 'string';
const key = plain ? language : language.languageId;
@@ -610,8 +652,8 @@ export class SettingsComponent extends React.Component<
event: React.ChangeEvent
): void => {
const { selectedIndex } = event.target.options;
- const theme = this.props.themeService.getThemes()[selectedIndex];
- if (theme) {
+ const theme = this.separatedThemes[selectedIndex];
+ if (theme && typeof theme !== 'string') {
this.setState({ themeId: theme.id });
if (this.props.themeService.getCurrentTheme().id !== theme.id) {
this.props.themeService.setCurrentTheme(theme.id);
diff --git a/arduino-ide-extension/src/browser/theia/core/theming.ts b/arduino-ide-extension/src/browser/theia/core/theming.ts
index 9f95f780d..b46f04b9b 100644
--- a/arduino-ide-extension/src/browser/theia/core/theming.ts
+++ b/arduino-ide-extension/src/browser/theia/core/theming.ts
@@ -1,15 +1,19 @@
-import type { Theme } from '@theia/core/lib/common/theme';
-import { injectable } from '@theia/core/shared/inversify';
-import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
+import {
+ BuiltinThemeProvider,
+ ThemeService,
+} from '@theia/core/lib/browser/theming';
+import { nls } from '@theia/core/lib/common/nls';
+import type { Theme, ThemeType } from '@theia/core/lib/common/theme';
+import { assertUnreachable } from '../../../common/utils';
export namespace ArduinoThemes {
- export const Light: Theme = {
+ export const light: Theme = {
id: 'arduino-theme',
type: 'light',
label: 'Light (Arduino)',
editorTheme: 'arduino-theme',
};
- export const Dark: Theme = {
+ export const dark: Theme = {
id: 'arduino-theme-dark',
type: 'dark',
label: 'Dark (Arduino)',
@@ -17,10 +21,166 @@ export namespace ArduinoThemes {
};
}
-@injectable()
-export class ThemeServiceWithDB extends TheiaThemeServiceWithDB {
- protected override init(): void {
- this.register(ArduinoThemes.Light, ArduinoThemes.Dark);
- super.init();
+const builtInThemeIds = new Set(
+ [
+ ArduinoThemes.light,
+ ArduinoThemes.dark,
+ BuiltinThemeProvider.hcTheme,
+ // TODO: add the HC light theme after Theia 1.36
+ ].map(({ id }) => id)
+);
+const deprecatedThemeIds = new Set(
+ [BuiltinThemeProvider.lightTheme, BuiltinThemeProvider.darkTheme].map(
+ ({ id }) => id
+ )
+);
+
+export const lightThemeLabel = nls.localize('arduino/theme/light', 'Light');
+export const darkThemeLabel = nls.localize('arduino/theme/dark', 'Dark');
+export const hcThemeLabel = nls.localize('arduino/theme/hc', 'High Contrast');
+export function userThemeLabel(theme: Theme): string {
+ return nls.localize('arduino/theme/user', '{0} (user)', theme.label);
+}
+export function deprecatedThemeLabel(theme: Theme): string {
+ return nls.localize(
+ 'arduino/theme/deprecated',
+ '{0} (deprecated)',
+ theme.label
+ );
+}
+
+export function themeLabelForSettings(theme: Theme): string {
+ switch (theme.id) {
+ case ArduinoThemes.light.id:
+ return lightThemeLabel;
+ case ArduinoThemes.dark.id:
+ return darkThemeLabel;
+ case BuiltinThemeProvider.hcTheme.id:
+ return hcThemeLabel;
+ case BuiltinThemeProvider.lightTheme.id: // fall-through
+ case BuiltinThemeProvider.darkTheme.id:
+ return deprecatedThemeLabel(theme);
+ default:
+ return userThemeLabel(theme);
+ }
+}
+
+export function compatibleBuiltInTheme(theme: Theme): Theme {
+ switch (theme.type) {
+ case 'light':
+ return ArduinoThemes.light;
+ case 'dark':
+ return ArduinoThemes.dark;
+ case 'hc':
+ return BuiltinThemeProvider.hcTheme;
+ default: {
+ console.warn(
+ `Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}`
+ );
+ return ArduinoThemes.light;
+ }
+ }
+}
+
+// For tests without DI
+interface ThemeProvider {
+ themes(): Theme[];
+ currentTheme(): Theme;
+}
+
+/**
+ * Returns with a list of built-in themes officially supported by IDE2 (https://github.com/arduino/arduino-ide/issues/1283).
+ * The themes in the array follow the following order:
+ * - built-in themes first (in `Light`, `Dark`, `High Contrast`), // TODO -> High Contrast will be split up to HC Dark and HC Light after the Theia version uplift
+ * - followed by user installed (VSIX) themes grouped by theme type, then alphabetical order,
+ * - if the `currentTheme` is either Light (Theia) or Dark (Theia), the last item of the array will be the selected theme with `(deprecated)` suffix.
+ */
+export function userConfigurableThemes(service: ThemeService): Theme[][];
+export function userConfigurableThemes(provider: ThemeProvider): Theme[][];
+export function userConfigurableThemes(
+ serviceOrProvider: ThemeService | ThemeProvider
+): Theme[][] {
+ const provider =
+ serviceOrProvider instanceof ThemeService
+ ? {
+ currentTheme: () => serviceOrProvider.getCurrentTheme(),
+ themes: () => serviceOrProvider.getThemes(),
+ }
+ : serviceOrProvider;
+ const currentTheme = provider.currentTheme();
+ const allThemes = provider
+ .themes()
+ .map((theme) => ({ ...theme, arduinoThemeType: arduinoThemeTypeOf(theme) }))
+ .filter(
+ (theme) =>
+ theme.arduinoThemeType !== 'deprecated' || currentTheme.id === theme.id
+ )
+ .sort((left, right) => {
+ const leftArduinoThemeType = left.arduinoThemeType;
+ const rightArduinoThemeType = right.arduinoThemeType;
+ if (leftArduinoThemeType === rightArduinoThemeType) {
+ const result = themeTypeOrder[left.type] - themeTypeOrder[right.type];
+ if (result) {
+ return result;
+ }
+ return left.label.localeCompare(right.label); // alphabetical order
+ }
+ return (
+ arduinoThemeTypeOrder[leftArduinoThemeType] -
+ arduinoThemeTypeOrder[rightArduinoThemeType]
+ );
+ });
+ const builtInThemes: Theme[] = [];
+ const userThemes: Theme[] = [];
+ const deprecatedThemes: Theme[] = [];
+ allThemes.forEach((theme) => {
+ const { arduinoThemeType } = theme;
+ switch (arduinoThemeType) {
+ case 'built-in':
+ builtInThemes.push(theme);
+ break;
+ case 'user':
+ userThemes.push(theme);
+ break;
+ case 'deprecated':
+ deprecatedThemes.push(theme);
+ break;
+ default:
+ assertUnreachable(arduinoThemeType);
+ }
+ });
+ const groupedThemes: Theme[][] = [];
+ if (builtInThemes.length) {
+ groupedThemes.push(builtInThemes);
+ }
+ if (userThemes.length) {
+ groupedThemes.push(userThemes);
+ }
+ if (deprecatedThemes.length) {
+ groupedThemes.push(deprecatedThemes);
+ }
+ return groupedThemes;
+}
+
+export type ArduinoThemeType = 'built-in' | 'user' | 'deprecated';
+const arduinoThemeTypeOrder: Record = {
+ 'built-in': 0,
+ user: 1,
+ deprecated: 2,
+};
+const themeTypeOrder: Record = {
+ light: 0,
+ dark: 1,
+ hc: 2,
+};
+
+export function arduinoThemeTypeOf(theme: Theme | string): ArduinoThemeType {
+ const themeId = typeof theme === 'string' ? theme : theme.id;
+ if (builtInThemeIds.has(themeId)) {
+ return 'built-in';
+ }
+ if (deprecatedThemeIds.has(themeId)) {
+ return 'deprecated';
}
+ return 'user';
}
diff --git a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts
index 4951ba771..40d703423 100644
--- a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts
+++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts
@@ -1,23 +1,231 @@
-import { injectable } from '@theia/core/shared/inversify';
-import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
-import { ArduinoThemes } from '../core/theming';
+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 {
- override initialize(): void {
- super.initialize();
- const { Light, Dark } = ArduinoThemes;
+ @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,
+ 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,
+ 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)
+ );
}
diff --git a/arduino-ide-extension/src/test/browser/theming.test.ts b/arduino-ide-extension/src/test/browser/theming.test.ts
new file mode 100644
index 000000000..89265e61a
--- /dev/null
+++ b/arduino-ide-extension/src/test/browser/theming.test.ts
@@ -0,0 +1,179 @@
+import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
+const disableJSDOM = enableJSDOM();
+
+import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming';
+import { Theme } from '@theia/core/lib/common/theme';
+import { expect } from 'chai';
+import {
+ ArduinoThemeType,
+ ArduinoThemes,
+ arduinoThemeTypeOf,
+ darkThemeLabel,
+ deprecatedThemeLabel,
+ hcThemeLabel,
+ lightThemeLabel,
+ themeLabelForSettings,
+ userConfigurableThemes,
+ userThemeLabel,
+} from '../../browser/theia/core/theming';
+
+disableJSDOM();
+
+const testTheme: Theme = {
+ id: 'testTheme',
+ label: 'Test Theme',
+ type: 'light',
+};
+const anotherTestTheme: Theme = {
+ id: 'anotherTestTheme',
+ label: 'Another Test Theme',
+ type: 'light',
+};
+const darkTestTheme: Theme = {
+ id: 'darkTestTheme',
+ label: 'Dark Test Theme',
+ type: 'dark',
+};
+const anotherDarkTestTheme: Theme = {
+ id: 'anotherTestTheme',
+ label: 'AAAnother Dark Test Theme',
+ type: 'dark',
+};
+
+describe('theming', () => {
+ describe('userConfigurableThemes', () => {
+ it('should show only built-in and user installed themes but not deprecated (Theia) ones if current theme is a built-in', () => {
+ const actual = userConfigurableThemes({
+ themes: () => [
+ BuiltinThemeProvider.darkTheme,
+ BuiltinThemeProvider.lightTheme,
+ ArduinoThemes.dark,
+ ArduinoThemes.light,
+ testTheme,
+ BuiltinThemeProvider.hcTheme,
+ anotherTestTheme,
+ ],
+ currentTheme: () => BuiltinThemeProvider.hcTheme,
+ }).reduce((acc, curr) => acc.concat(curr), []);
+ expect(actual.length).to.be.equal(5);
+ expect(actual[0].id).to.be.equal(ArduinoThemes.light.id);
+ expect(actual[1].id).to.be.equal(ArduinoThemes.dark.id);
+ expect(actual[2].id).to.be.equal(BuiltinThemeProvider.hcTheme.id);
+ expect(actual[3].id).to.be.equal(anotherTestTheme.id);
+ expect(actual[4].id).to.be.equal(testTheme.id);
+ });
+
+ it('should show only built-in and user installed themes but not deprecated (Theia) ones if current theme is a user', () => {
+ const actual = userConfigurableThemes({
+ themes: () => [
+ BuiltinThemeProvider.hcTheme,
+ BuiltinThemeProvider.lightTheme,
+ BuiltinThemeProvider.darkTheme,
+ ArduinoThemes.dark,
+ testTheme,
+ anotherTestTheme,
+ ArduinoThemes.light,
+ ],
+ currentTheme: () => testTheme,
+ }).reduce((acc, curr) => acc.concat(curr), []);
+ expect(actual.length).to.be.equal(5);
+ expect(actual[0].id).to.be.equal(ArduinoThemes.light.id);
+ expect(actual[1].id).to.be.equal(ArduinoThemes.dark.id);
+ expect(actual[2].id).to.be.equal(BuiltinThemeProvider.hcTheme.id);
+ expect(actual[3].id).to.be.equal(anotherTestTheme.id);
+ expect(actual[4].id).to.be.equal(testTheme.id);
+ });
+
+ it('should show built-in, user installed, and deprecated (Theia) themes if current theme is a deprecated (Theia)', () => {
+ const actual = userConfigurableThemes({
+ themes: () => [
+ ArduinoThemes.dark,
+ ArduinoThemes.light,
+ testTheme,
+ BuiltinThemeProvider.hcTheme,
+ anotherTestTheme,
+ darkTestTheme,
+ anotherDarkTestTheme,
+ BuiltinThemeProvider.lightTheme,
+ BuiltinThemeProvider.darkTheme,
+ ],
+ currentTheme: () => BuiltinThemeProvider.lightTheme,
+ }).reduce((acc, curr) => acc.concat(curr), []);
+ expect(actual.length).to.be.equal(8);
+ expect(actual[0].id).to.be.equal(ArduinoThemes.light.id);
+ expect(actual[1].id).to.be.equal(ArduinoThemes.dark.id);
+ expect(actual[2].id).to.be.equal(BuiltinThemeProvider.hcTheme.id);
+ expect(actual[3].id).to.be.equal(anotherTestTheme.id);
+ expect(actual[4].id).to.be.equal(testTheme.id);
+ expect(actual[5].id).to.be.equal(anotherDarkTestTheme.id);
+ expect(actual[6].id).to.be.equal(darkTestTheme.id);
+ expect(actual[7].id).to.be.equal(BuiltinThemeProvider.lightTheme.id);
+ });
+
+ it('should group the themes by arduino theme types', () => {
+ const actual = userConfigurableThemes({
+ themes: () => [
+ ArduinoThemes.dark,
+ ArduinoThemes.light,
+ testTheme,
+ BuiltinThemeProvider.hcTheme,
+ anotherTestTheme,
+ darkTestTheme,
+ anotherDarkTestTheme,
+ BuiltinThemeProvider.lightTheme,
+ BuiltinThemeProvider.darkTheme,
+ ],
+ currentTheme: () => BuiltinThemeProvider.lightTheme,
+ });
+ expect(actual.length).to.be.equal(3);
+ expect(actual[0].length).to.be.equal(3);
+ expect(actual[1].length).to.be.equal(4);
+ expect(actual[2].length).to.be.equal(1);
+ });
+ });
+
+ describe('arduinoThemeTypeOf', () => {
+ (
+ [
+ [BuiltinThemeProvider.lightTheme, 'deprecated'],
+ [BuiltinThemeProvider.darkTheme, 'deprecated'],
+ [BuiltinThemeProvider.hcTheme, 'built-in'],
+ [ArduinoThemes.light, 'built-in'],
+ [ArduinoThemes.dark, 'built-in'],
+ [testTheme, 'user'],
+ [anotherTestTheme, 'user'],
+ [darkTestTheme, 'user'],
+ [anotherDarkTestTheme, 'user'],
+ ] as [Theme, ArduinoThemeType][]
+ ).map(([theme, expected]) =>
+ it(`should detect the '${theme.label}' theme as '${expected}' theme`, () =>
+ expect(arduinoThemeTypeOf(theme)).to.be.equal(expected))
+ );
+ });
+
+ describe('themeLabelForSettings', () => {
+ (
+ [
+ [
+ BuiltinThemeProvider.lightTheme,
+ deprecatedThemeLabel(BuiltinThemeProvider.lightTheme),
+ ],
+ [
+ BuiltinThemeProvider.darkTheme,
+ deprecatedThemeLabel(BuiltinThemeProvider.darkTheme),
+ ],
+ [BuiltinThemeProvider.hcTheme, hcThemeLabel],
+ [ArduinoThemes.light, lightThemeLabel],
+ [ArduinoThemes.dark, darkThemeLabel],
+ [testTheme, userThemeLabel(testTheme)],
+ [anotherTestTheme, userThemeLabel(anotherTestTheme)],
+ [darkTestTheme, userThemeLabel(darkTestTheme)],
+ [anotherDarkTestTheme, userThemeLabel(anotherDarkTestTheme)],
+ ] as [Theme, string][]
+ ).map(([theme, expected]) => {
+ it(`should map the theme with ID '${theme.id}' to ${expected} in the settings UI`, () => {
+ expect(themeLabelForSettings(theme)).to.be.equal(expected);
+ });
+ });
+ });
+});
diff --git a/electron/build/scripts/arduino-ide-electron-main.js b/electron/build/scripts/arduino-ide-electron-main.js
index 82a63fbec..12ad02e72 100644
--- a/electron/build/scripts/arduino-ide-electron-main.js
+++ b/electron/build/scripts/arduino-ide-electron-main.js
@@ -1,9 +1,15 @@
const os = require('os');
const path = require('path');
// Enables the discovery of the VS Code extensions in the embedded `plugins` folder in the final app.
-process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${path.resolve(__dirname, '..', 'plugins')}`;
+process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${path.resolve(
+ __dirname,
+ '..',
+ 'plugins'
+)}`;
process.env.THEIA_PLUGINS = [
- process.env.THEIA_PLUGINS,
- `local-dir:${path.resolve(os.homedir(), '.arduinoProIDE', 'plugins')}`
-].filter(Boolean).join(',');
+ process.env.THEIA_PLUGINS,
+ `local-dir:${path.resolve(os.homedir(), '.arduinoIDE', 'plugins')}`,
+]
+ .filter(Boolean)
+ .join(',');
require('../src-gen/frontend/electron-main.js');
diff --git a/i18n/en.json b/i18n/en.json
index 320be23f1..08a47309e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -474,6 +474,14 @@
"dismissSurvey": "Don't show again",
"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."
},
+ "theme": {
+ "currentThemeNotFound": "Could not find the currently selected theme: {0}. Arduino IDE has picked a built-in theme compatible with the missing one.",
+ "dark": "Dark",
+ "deprecated": "{0} (deprecated)",
+ "hc": "High Contrast",
+ "light": "Light",
+ "user": "{0} (user)"
+ },
"title": {
"cloud": "Cloud"
},