From 3676960d4bb40bade9d9d666d23b5441235155ce Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 3 Apr 2023 16:50:09 +0200 Subject: [PATCH 1/2] fix: location of possible drop-in folder for VSIX Closes #1851 Signed-off-by: Akos Kitta --- .../build/scripts/arduino-ide-electron-main.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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'); From 417c2b7086e2bc5fdf89492dbea2cb1a089fac82 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 3 Apr 2023 15:33:46 +0200 Subject: [PATCH 2/2] feat: removed the non official themes from the UI Closes #1283 Ref eclipse-theia/theia#11151 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 23 +- .../dialogs/settings/settings-component.tsx | 58 ++++- .../src/browser/theia/core/theming.ts | 180 +++++++++++++- .../theia/monaco/monaco-theming-service.ts | 228 +++++++++++++++++- .../src/test/browser/theming.test.ts | 179 ++++++++++++++ i18n/en.json | 8 + 6 files changed, 642 insertions(+), 34 deletions(-) create mode 100644 arduino-ide-extension/src/test/browser/theming.test.ts 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/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" },