diff --git a/.gitignore b/.gitignore index 4380cce54..b683e4e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ build/ Examples/ !electron/build/ src-gen/ -!webpack.config.js +webpack.config.js gen-webpack.config.js .DS_Store # switching from `electron` to `browser` in dev mode. @@ -15,8 +15,6 @@ gen-webpack.config.js yarn*.log # For the VS Code extensions used by Theia. plugins -# the config files for the CLI -arduino-ide-extension/data/cli/config # the tokens folder for the themes scripts/themes/tokens # environment variables diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 6d88f9cf4..98dc9c0a9 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -21,21 +21,21 @@ }, "dependencies": { "@grpc/grpc-js": "^1.6.7", - "@theia/application-package": "1.25.0", - "@theia/core": "1.25.0", - "@theia/editor": "1.25.0", - "@theia/electron": "1.25.0", - "@theia/filesystem": "1.25.0", - "@theia/keymaps": "1.25.0", - "@theia/markers": "1.25.0", - "@theia/monaco": "1.25.0", - "@theia/navigator": "1.25.0", - "@theia/outline-view": "1.25.0", - "@theia/output": "1.25.0", - "@theia/preferences": "1.25.0", - "@theia/search-in-workspace": "1.25.0", - "@theia/terminal": "1.25.0", - "@theia/workspace": "1.25.0", + "@theia/application-package": "1.31.1", + "@theia/core": "1.31.1", + "@theia/editor": "1.31.1", + "@theia/electron": "1.31.1", + "@theia/filesystem": "1.31.1", + "@theia/keymaps": "1.31.1", + "@theia/markers": "1.31.1", + "@theia/monaco": "1.31.1", + "@theia/navigator": "1.31.1", + "@theia/outline-view": "1.31.1", + "@theia/output": "1.31.1", + "@theia/preferences": "1.31.1", + "@theia/search-in-workspace": "1.31.1", + "@theia/terminal": "1.31.1", + "@theia/workspace": "1.31.1", "@tippyjs/react": "^4.2.5", "@types/atob": "^2.1.2", "@types/auth0-js": "^9.14.0", @@ -50,9 +50,10 @@ "@types/lodash.debounce": "^4.0.6", "@types/ncp": "^2.0.4", "@types/node-fetch": "^2.5.7", + "@types/p-queue": "^2.3.1", "@types/ps-tree": "^1.1.0", - "@types/react-select": "^3.0.0", "@types/react-tabs": "^2.3.2", + "@types/react-virtualized": "^9.21.21", "@types/temp": "^0.8.34", "@types/which": "^1.3.1", "ajv": "^6.5.3", @@ -78,13 +79,14 @@ "ncp": "^2.0.0", "node-fetch": "^2.6.1", "open": "^8.0.6", - "p-queue": "^5.0.0", + "p-queue": "^2.4.2", "ps-tree": "^1.2.0", "query-string": "^7.0.1", - "react-disable": "^0.1.0", + "react-disable": "^0.1.1", "react-markdown": "^8.0.0", - "react-select": "^3.0.4", + "react-select": "^5.6.0", "react-tabs": "^3.1.2", + "react-virtualized": "^9.22.3", "react-window": "^1.8.6", "semver": "^7.3.2", "string-natural-compare": "^2.0.3", 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 dfeffb0cb..b56eef9df 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -3,10 +3,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; import { CommandContribution } from '@theia/core/lib/common/command'; import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; -import { - TabBarToolbarContribution, - TabBarToolbarFactory, -} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { FrontendApplicationContribution, @@ -84,10 +81,7 @@ import { BoardsAutoInstaller } from './boards/boards-auto-installer'; import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer'; import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; -import { - MonacoThemeJson, - MonacoThemingService, -} from '@theia/monaco/lib/browser/monaco-theming-service'; + import { ArduinoDaemonPath, ArduinoDaemon, @@ -137,7 +131,6 @@ import { Settings } from './contributions/settings'; import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands'; import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler'; import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler'; -import { TabBarToolbar } from './theia/core/tab-bar-toolbar'; import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory'; import { EditorWidgetFactory } from './theia/editor/editor-widget-factory'; import { BurnBootloader } from './contributions/burn-bootloader'; @@ -181,8 +174,6 @@ import { EditorCommandContribution } from './theia/editor/editor-command'; import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator'; import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator'; import { Debug } from './contributions/debug'; -import { DebugSessionManager } from './theia/debug/debug-session-manager'; -import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { Sketchbook } from './contributions/sketchbook'; import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution'; import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution'; @@ -313,10 +304,6 @@ import { SelectedBoard } from './contributions/selected-board'; import { CheckForIDEUpdates } from './contributions/check-for-ide-updates'; import { OpenBoardsConfig } from './contributions/open-boards-config'; import { SketchFilesTracker } from './contributions/sketch-files-tracker'; -import { MonacoThemeServiceIsReady } from './utils/window'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { StatusBarImpl } from './theia/core/status-bar'; -import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser'; import { EditorMenuContribution } from './theia/editor/editor-file'; import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu'; import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget'; @@ -337,32 +324,12 @@ import { InterfaceScale } from './contributions/interface-scale'; import { OpenHandler } from '@theia/core/lib/browser/opener-service'; import { NewCloudSketch } from './contributions/new-cloud-sketch'; import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget'; - -const registerArduinoThemes = () => { - const themes: MonacoThemeJson[] = [ - { - id: 'arduino-theme', - label: 'Light (Arduino)', - uiTheme: 'vs', - json: require('../../src/browser/data/default.color-theme.json'), - }, - { - id: 'arduino-theme-dark', - label: 'Dark (Arduino)', - uiTheme: 'vs-dark', - json: require('../../src/browser/data/dark.color-theme.json'), - }, - ]; - themes.forEach((theme) => MonacoThemingService.register(theme)); -}; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const global = window as any; -const ready = global[MonacoThemeServiceIsReady] as Deferred; -if (ready) { - ready.promise.then(registerArduinoThemes); -} else { - registerArduinoThemes(); -} +import { WindowTitleUpdater } from './theia/core/window-title-updater'; +import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater'; +import { ThemeService } from './theia/core/theming'; +import { ThemeService as TheiaThemeService } from '@theia/core/lib/browser/theming'; +import { MonacoThemingService } from './theia/monaco/monaco-theming-service'; +import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -587,14 +554,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .to(WorkspaceDeleteHandler) .inSingletonScope(); rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope(); - rebind(TabBarToolbarFactory).toFactory( - ({ container: parentContainer }) => - () => { - const container = parentContainer.createChild(); - container.bind(TabBarToolbar).toSelf().inSingletonScope(); - return container.get(TabBarToolbar); - } - ); bind(OutputChannelManager).toSelf().inSingletonScope(); rebind(TheiaOutputChannelManager).toService(OutputChannelManager); bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope(); @@ -838,9 +797,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(AboutDialog).toSelf().inSingletonScope(); rebind(TheiaAboutDialog).toService(AboutDialog); - // To avoid running `Save All` when there are no dirty editors before starting the debug session. - bind(DebugSessionManager).toSelf().inSingletonScope(); - rebind(TheiaDebugSessionManager).toService(DebugSessionManager); // To remove the `Run` menu item from the application menu. bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope(); rebind(TheiaDebugFrontendApplicationContribution).toService( @@ -854,10 +810,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WidgetManager).toSelf().inSingletonScope(); rebind(TheiaWidgetManager).toService(WidgetManager); - // To avoid running a status bar update on every single `keypress` event from the editor. - bind(StatusBarImpl).toSelf().inSingletonScope(); - rebind(TheiaStatusBarImpl).toService(StatusBarImpl); - // Debounced update for the tab-bar toolbar when typing in the editor. bind(DockPanelRenderer).toSelf(); rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer); @@ -942,7 +894,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(LocalCacheFsProvider).toSelf().inSingletonScope(); bind(FileServiceContribution).toService(LocalCacheFsProvider); bind(CloudSketchbookCompositeWidget).toSelf(); - bind(WidgetFactory).toDynamicValue((ctx) => ({ + bind(WidgetFactory).toDynamicValue((ctx) => ({ id: 'cloud-sketchbook-composite-widget', createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget), })); @@ -991,4 +943,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport); bind(HostedPluginEvents).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(HostedPluginEvents); + + // custom window titles + bind(WindowTitleUpdater).toSelf().inSingletonScope(); + rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater); + + // register Arduino themes + bind(ThemeService).toSelf().inSingletonScope(); + rebind(TheiaThemeService).toService(ThemeService); + bind(MonacoThemingService).toSelf().inSingletonScope(); + rebind(TheiaMonacoThemingService).toService(MonacoThemingService); }); diff --git a/arduino-ide-extension/src/browser/contributions/sketch-control.ts b/arduino-ide-extension/src/browser/contributions/sketch-control.ts index 62f2d8ce8..216275450 100644 --- a/arduino-ide-extension/src/browser/contributions/sketch-control.ts +++ b/arduino-ide-extension/src/browser/contributions/sketch-control.ts @@ -25,6 +25,7 @@ import { } from '../../common/protocol/sketches-service-client-impl'; import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider'; import { nls } from '@theia/core/lib/common'; +import { codicon } from '@theia/core/lib/browser/widgets/widget'; @injectable() export class SketchControl extends SketchContribution { @@ -235,7 +236,7 @@ export class SketchControl extends SketchContribution { }); registry.registerKeybinding({ command: CommonCommands.PREVIOUS_TAB.id, - keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI. + keybinding: 'CtrlCmd+Alt+Left', }); registry.registerKeybinding({ command: CommonCommands.NEXT_TAB.id, @@ -276,7 +277,7 @@ export namespace SketchControl { export namespace Commands { export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = { id: 'arduino-open-sketch-control--toolbar', - iconClass: 'fa fa-arduino-sketch-tabs-menu', + iconClass: codicon('ellipsis'), }; } } diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx index 93da416e5..fa9466fcf 100644 --- a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx @@ -1,7 +1,7 @@ import { nls } from '@theia/core/lib/common'; import { shell } from 'electron'; import * as React from '@theia/core/shared/react'; -import * as ReactDOM from '@theia/core/shared/react-dom'; +import { createRoot } from '@theia/core/shared/react-dom/client'; import ReactMarkdown from 'react-markdown'; import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater'; import ProgressBar from '../../components/ProgressBar'; @@ -30,6 +30,7 @@ export const IDEUpdaterComponent = ({ const { version, releaseNotes } = updateInfo; const changelogDivRef = React.useRef() as React.MutableRefObject; + const changelogRoot = createRoot(changelogDivRef.current); React.useEffect(() => { if (!!releaseNotes && changelogDivRef.current) { let changelog: string; @@ -38,7 +39,7 @@ export const IDEUpdaterComponent = ({ changelog = releaseNotes.reduce((acc, item) => { return item.note ? (acc += `${item.note}\n\n`) : acc; }, ''); - ReactDOM.render( + changelogRoot.render( ( @@ -49,8 +50,7 @@ export const IDEUpdaterComponent = ({ }} > {changelog} - , - changelogDivRef.current + ); } }, [updateInfo]); 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 3138c7242..414cee7d7 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx @@ -218,16 +218,14 @@ export class SettingsComponent extends React.Component<
@@ -612,11 +610,11 @@ export class SettingsComponent extends React.Component< event: React.ChangeEvent ): void => { const { selectedIndex } = event.target.options; - const theme = ThemeService.get().getThemes()[selectedIndex]; + const theme = this.props.themeService.getThemes()[selectedIndex]; if (theme) { this.setState({ themeId: theme.id }); - if (ThemeService.get().getCurrentTheme().id !== theme.id) { - ThemeService.get().setCurrentTheme(theme.id); + if (this.props.themeService.getCurrentTheme().id !== theme.id) { + this.props.themeService.setCurrentTheme(theme.id); } } }; @@ -755,6 +753,7 @@ export namespace SettingsComponent { readonly fileDialogService: FileDialogService; readonly windowService: WindowService; readonly localizationProvider: AsyncLocalizationProvider; + readonly themeService: ThemeService; } export type State = Settings & { rawAdditionalUrlsValue: string; diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx index 7ebc7c5ba..0c9e51e43 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx @@ -35,6 +35,9 @@ export class SettingsWidget extends ReactWidget { @inject(AsyncLocalizationProvider) protected readonly localizationProvider: AsyncLocalizationProvider; + @inject(ThemeService) + private readonly themeService: ThemeService; + protected render(): React.ReactNode { return ( ); } @@ -59,6 +63,9 @@ export class SettingsDialog extends AbstractDialog> { @inject(SettingsWidget) protected readonly widget: SettingsWidget; + @inject(ThemeService) + private readonly themeService: ThemeService; + constructor( @inject(SettingsDialogProps) protected override readonly props: SettingsDialogProps @@ -121,11 +128,11 @@ export class SettingsDialog extends AbstractDialog> { } override async open(): Promise | undefined> { - const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id; + const themeIdBeforeOpen = this.themeService.getCurrentTheme().id; const result = await super.open(); if (!result) { - if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) { - ThemeService.get().setCurrentTheme(themeIdBeforeOpen); + if (this.themeService.getCurrentTheme().id !== themeIdBeforeOpen) { + this.themeService.setCurrentTheme(themeIdBeforeOpen); } } return result; diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings.ts b/arduino-ide-extension/src/browser/dialogs/settings/settings.ts index 7033f8e8a..c8c0344ec 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings.ts +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings.ts @@ -5,7 +5,7 @@ import { } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { Emitter } from '@theia/core/lib/common/event'; -import { Deferred, timeout } from '@theia/core/lib/common/promise-util'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { deepClone } from '@theia/core/lib/common/objects'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { ThemeService } from '@theia/core/lib/browser/theming'; @@ -25,6 +25,8 @@ import { LanguageInfo, } from '@theia/core/lib/common/i18n/localization'; import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'; +import { DefaultTheme } from '@theia/application-package/lib/application-props'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; export const EDITOR_SETTING = 'editor'; export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`; @@ -101,6 +103,9 @@ export class SettingsService { @inject(CommandService) protected commandService: CommandService; + @inject(ThemeService) + private readonly themeService: ThemeService; + protected readonly onDidChangeEmitter = new Emitter>(); readonly onDidChange = this.onDidChangeEmitter.event; protected readonly onDidResetEmitter = new Emitter>(); @@ -141,10 +146,9 @@ export class SettingsService { this.preferenceService.get(FONT_SIZE_SETTING, 12), this.preferenceService.get( 'workbench.colorTheme', - window.matchMedia && - window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'arduino-theme-dark' - : 'arduino-theme' + DefaultTheme.defaultForOSTheme( + FrontendApplicationConfigProvider.get().defaultTheme + ) ), this.preferenceService.get( AUTO_SAVE_SETTING, @@ -231,11 +235,7 @@ export class SettingsService { 'Invalid editor font size. It must be a positive integer.' ); } - if ( - !ThemeService.get() - .getThemes() - .find(({ id }) => id === themeId) - ) { + if (!this.themeService.getThemes().find(({ id }) => id === themeId)) { return nls.localize( 'arduino/preferences/invalid.theme', 'Invalid theme.' @@ -252,7 +252,6 @@ export class SettingsService { private async savePreference(name: string, value: unknown): Promise { await this.preferenceService.set(name, value, PreferenceScope.User); - await timeout(5); } async save(): Promise { @@ -283,19 +282,21 @@ export class SettingsService { (config as any).network = network; (config as any).locale = currentLanguage; - await this.savePreference('editor.fontSize', editorFontSize); - await this.savePreference('workbench.colorTheme', themeId); - await this.savePreference(AUTO_SAVE_SETTING, autoSave); - await this.savePreference('editor.quickSuggestions', quickSuggestions); - await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface); - await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale); - await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale); - await this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile); - await this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings); - await this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload); - await this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload); - await this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles); - await this.configService.setConfiguration(config); + await Promise.all([ + this.savePreference('editor.fontSize', editorFontSize), + this.savePreference('workbench.colorTheme', themeId), + this.savePreference(AUTO_SAVE_SETTING, autoSave), + this.savePreference('editor.quickSuggestions', quickSuggestions), + this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface), + this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale), + this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale), + this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile), + this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings), + this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload), + this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload), + this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles), + this.configService.setConfiguration(config), + ]); this.onDidChangeEmitter.fire(this._settings); // after saving all the settings, if we need to change the language we need to perform a reload diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index f9aba5ed4..a5a25230c 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -1,6 +1,5 @@ import * as React from '@theia/core/shared/react'; import { injectable, inject } from '@theia/core/shared/inversify'; -import { OptionsType } from 'react-select/src/types'; import { Emitter } from '@theia/core/lib/common/event'; import { Disposable } from '@theia/core/lib/common/disposable'; import { @@ -128,9 +127,7 @@ export class MonitorWidget extends ReactWidget { ); }; - protected get lineEndings(): OptionsType< - SerialMonitorOutput.SelectOption - > { + protected get lineEndings(): SerialMonitorOutput.SelectOption[] { return [ { label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'), diff --git a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts index 0785cad03..c03b18869 100644 --- a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts @@ -22,7 +22,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution CommonCommands.TOGGLE_MAXIMIZED, CommonCommands.PIN_TAB, CommonCommands.UNPIN_TAB, - CommonCommands.NEW_FILE, + CommonCommands.NEW_UNTITLED_FILE, ]) { commandRegistry.unregisterCommand(command); } diff --git a/arduino-ide-extension/src/browser/theia/core/status-bar.ts b/arduino-ide-extension/src/browser/theia/core/status-bar.ts deleted file mode 100644 index 3e7782c3b..000000000 --- a/arduino-ide-extension/src/browser/theia/core/status-bar.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { injectable } from '@theia/core/shared/inversify'; -import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser'; - -@injectable() -export class StatusBarImpl extends TheiaStatusBarImpl { - override async removeElement(id: string): Promise { - await this.ready; - if (this.entries.delete(id)) { - // Unlike Theia, IDE2 updates the status bar only if the element to remove was among the entries. Otherwise, it's a NOOP. - this.update(); - } - } -} diff --git a/arduino-ide-extension/src/browser/theia/core/tab-bar-toolbar.tsx b/arduino-ide-extension/src/browser/theia/core/tab-bar-toolbar.tsx deleted file mode 100644 index 42e086d2b..000000000 --- a/arduino-ide-extension/src/browser/theia/core/tab-bar-toolbar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from '@theia/core/shared/react'; -import { injectable } from '@theia/core/shared/inversify'; -import { LabelIcon } from '@theia/core/lib/browser/label-parser'; -import { - TabBarToolbar as TheiaTabBarToolbar, - TabBarToolbarItem, -} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; - -@injectable() -export class TabBarToolbar extends TheiaTabBarToolbar { - /** - * Copied over from Theia. Added an ID to the parent of the toolbar item (`--container`). - * CSS3 does not support parent selectors but we want to style the parent of the toolbar item. - */ - protected override renderItem(item: TabBarToolbarItem): React.ReactNode { - let innerText = ''; - const classNames = []; - if (item.text) { - for (const labelPart of this.labelParser.parse(item.text)) { - if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) { - const className = `fa fa-${labelPart.name}${ - labelPart.animation ? ' fa-' + labelPart.animation : '' - }`; - classNames.push(...className.split(' ')); - } else { - innerText = labelPart; - } - } - } - const command = this.commands.getCommand(item.command); - const iconClass = - (typeof item.icon === 'function' && item.icon()) || - item.icon || - (command && command.iconClass); - if (iconClass) { - classNames.push(iconClass); - } - const tooltip = item.tooltip || (command && command.label); - return ( -
-
- {innerText} -
-
- ); - } -} diff --git a/arduino-ide-extension/src/browser/theia/core/theming.ts b/arduino-ide-extension/src/browser/theia/core/theming.ts new file mode 100644 index 000000000..78434b9a0 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/theming.ts @@ -0,0 +1,31 @@ +import { ThemeService as TheiaThemeService } from '@theia/core/lib/browser/theming'; +import type { Theme } from '@theia/core/lib/common/theme'; +import { injectable } from '@theia/core/shared/inversify'; + +export namespace ArduinoThemes { + export const Light: Theme = { + id: 'arduino-theme', + type: 'light', + label: 'Light (Arduino)', + editorTheme: 'arduino-theme', + }; + export const Dark: Theme = { + id: 'arduino-theme-dark', + type: 'dark', + label: 'Dark (Arduino)', + editorTheme: 'arduino-theme-dark', + }; + export const Default = + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ? Dark + : Light; +} + +@injectable() +export class ThemeService extends TheiaThemeService { + protected override init(): void { + this.register(ArduinoThemes.Light, ArduinoThemes.Dark); + super.init(); + } +} diff --git a/arduino-ide-extension/src/browser/theia/core/widget-manager.ts b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts index 2e98c2bfc..038b046a1 100644 --- a/arduino-ide-extension/src/browser/theia/core/widget-manager.ts +++ b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts @@ -1,4 +1,3 @@ -import type { MaybePromise } from '@theia/core'; import type { Widget } from '@theia/core/lib/browser'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; import { @@ -8,7 +7,6 @@ import { } from '@theia/core/shared/inversify'; import { EditorWidget } from '@theia/editor/lib/browser'; import { OutputWidget } from '@theia/output/lib/browser/output-widget'; -import deepEqual = require('deep-equal'); import { CurrentSketch, SketchesServiceClientImpl, @@ -72,44 +70,4 @@ export class WidgetManager extends TheiaWidgetManager { title.className += title.className + ` ${uncloseableClass}`; } } - - /** - * Customized to find any existing widget based on `options` deepEquals instead of string equals. - * See https://github.com/eclipse-theia/theia/issues/11309. - */ - protected override doGetWidget( - key: string - ): MaybePromise | undefined { - const pendingWidget = this.findExistingWidget(key); - if (pendingWidget) { - return pendingWidget as MaybePromise; - } - return undefined; - } - - private findExistingWidget( - key: string - ): MaybePromise | undefined { - const parsed = this.parseJson(key); - for (const [candidateKey, widget] of [ - ...this.widgetPromises.entries(), - ...this.pendingWidgetPromises.entries(), - ]) { - const candidate = this.parseJson(candidateKey); - if (deepEqual(candidate, parsed)) { - return widget as MaybePromise; - } - } - return undefined; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private parseJson(json: string): any { - try { - return JSON.parse(json); - } catch (err) { - console.log(`Failed to parse JSON: <${json}>.`, err); - throw err; - } - } } diff --git a/arduino-ide-extension/src/browser/theia/core/window-title-updater.ts b/arduino-ide-extension/src/browser/theia/core/window-title-updater.ts new file mode 100644 index 000000000..7ad488ea9 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/window-title-updater.ts @@ -0,0 +1,66 @@ +import { NavigatableWidget } from '@theia/core/lib/browser'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater'; +import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; + +@injectable() +export class WindowTitleUpdater extends TheiaWindowTitleUpdater { + @inject(ApplicationServer) + private readonly applicationServer: ApplicationServer; + @inject(ApplicationShell) + private readonly applicationShell: ApplicationShell; + @inject(WorkspaceService) + private readonly workspaceService: WorkspaceService; + + private readonly applicationName = + FrontendApplicationConfigProvider.get().applicationName; + private applicationVersion: string | undefined; + + @postConstruct() + protected init(): void { + setTimeout( + () => + this.applicationServer.getApplicationInfo().then((info) => { + this.applicationVersion = info?.version; + if (this.applicationVersion) { + this.handleWidgetChange(this.applicationShell.currentWidget); + } + }), + 0 + ); + } + + protected override handleWidgetChange(widget?: Widget | undefined): void { + // Unlike Theia, IDE2 does not want to show in the window title if the current widget is dirty or now. + // Hence, IDE2 does not track widgets but updates the window title on current widget change. + this.updateTitleWidget(widget); + } + + protected override updateTitleWidget(widget?: Widget | undefined): void { + let activeEditorShort = ''; + const rootName = this.workspaceService.workspace?.name ?? ''; + let appName = `${this.applicationName}${ + this.applicationVersion ? ` ${this.applicationVersion}` : '' + }`; + if (rootName) { + appName = ` | ${appName}`; + } + const uri = NavigatableWidget.getUri(widget); + if (uri) { + const base = uri.path.base; + // Do not show the basename of the main sketch file. Only other sketch file names are visible in the title. + if (`${rootName}.ino` !== base) { + activeEditorShort = ` - ${base} `; + } + } + this.windowTitleService.update({ rootName, appName, activeEditorShort }); + } +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts index 0059f433c..2523c99c8 100644 --- a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts @@ -1,5 +1,9 @@ import debounce = require('p-debounce'); -import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @@ -126,7 +130,7 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager { const uri = tempFolderUri.resolve('launch.json'); const { value } = await this.fileService.read(uri); const configurations = DebugConfigurationModel.parse(JSON.parse(value)); - return { uri, configurations }; + return { uri, configurations, compounds: [] }; } catch (err) { if ( err instanceof FileOperationError && diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts index 225a003c1..4eaadf172 100644 --- a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts @@ -29,6 +29,7 @@ export class DebugConfigurationModel extends TheiaDebugConfigurationModel { return { uri: this.configUri, configurations: this.config, + compounds: [], }; } } diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts deleted file mode 100644 index 6eb2ebdeb..000000000 --- a/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { injectable } from '@theia/core/shared/inversify'; -import { DebugError } from '@theia/debug/lib/common/debug-service'; -import { DebugSession } from '@theia/debug/lib/browser/debug-session'; -import { DebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; -import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; -import { nls } from '@theia/core/lib/common'; - -@injectable() -export class DebugSessionManager extends TheiaDebugSessionManager { - override async start(options: DebugSessionOptions): Promise { - return this.progressService.withProgress( - nls.localize('theia/debug/start', 'Start...'), - 'debug', - async () => { - try { - // Only save when dirty. To avoid saving temporary sketches. - // This is a quick fix for not saving the editor when there are no dirty editors. - // // https://github.com/bcmi-labs/arduino-editor/pull/172#issuecomment-741831888 - if (this.shell.canSaveAll()) { - await this.shell.saveAll(); - } - await this.fireWillStartDebugSession(); - const resolved = await this.resolveConfiguration(options); - - //#region "cherry-picked" from here: https://github.com/eclipse-theia/theia/commit/e6b57ba4edabf797f3b4e67bc2968cdb8cc25b1e#diff-08e04edb57cd2af199382337aaf1dbdb31171b37ae4ab38a38d36cd77bc656c7R196-R207 - if (!resolved) { - // As per vscode API: https://code.visualstudio.com/api/references/vscode-api#DebugConfigurationProvider - // "Returning the value 'undefined' prevents the debug session from starting. - // Returning the value 'null' prevents the debug session from starting and opens the - // underlying debug configuration instead." - - if (resolved === null) { - this.debugConfigurationManager.openConfiguration(); - } - return undefined; - } - //#endregion end of cherry-pick - - // preLaunchTask isn't run in case of auto restart as well as postDebugTask - if (!options.configuration.__restart) { - const taskRun = await this.runTask( - options.workspaceFolderUri, - resolved.configuration.preLaunchTask, - true - ); - if (!taskRun) { - return undefined; - } - } - - const sessionId = await this.debug.createDebugSession( - resolved.configuration - ); - return this.doStart(sessionId, resolved); - } catch (e) { - if (DebugError.NotFound.is(e)) { - this.messageService.error( - nls.localize( - 'theia/debug/typeNotSupported', - 'The debug session type "{0}" is not supported.', - e.data.type - ) - ); - return undefined; - } - - this.messageService.error( - nls.localize( - 'theia/debug/startError', - 'There was an error starting the debug session, check the logs for more details.' - ) - ); - console.error('Error starting the debug session', e); - throw e; - } - } - ); - } - override async terminateSession(session?: DebugSession): Promise { - if (!session) { - this.updateCurrentSession(this._currentSession); - session = this._currentSession; - } - // The cortex-debug extension does not respond to close requests - // So we simply terminate the debug session immediately - // Alternatively the `super.terminateSession` call will terminate it after 5 seconds without a response - await this.debug.terminateDebugSession(session!.id); - await super.terminateSession(session); - } -} diff --git a/arduino-ide-extension/src/browser/theia/editor/editor-widget-factory.ts b/arduino-ide-extension/src/browser/theia/editor/editor-widget-factory.ts index fc368c90d..3df32188c 100644 --- a/arduino-ide-extension/src/browser/theia/editor/editor-widget-factory.ts +++ b/arduino-ide-extension/src/browser/theia/editor/editor-widget-factory.ts @@ -20,7 +20,7 @@ export class EditorWidgetFactory extends TheiaEditorWidgetFactory { protected override async createEditor( uri: URI, - options: NavigatableWidgetOptions + options?: NavigatableWidgetOptions ): Promise { const widget = await super.createEditor(uri, options); return this.maybeUpdateCaption(widget); diff --git a/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx b/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx index 56652d66a..89ef20724 100644 --- a/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx +++ b/arduino-ide-extension/src/browser/theia/messages/notifications-renderer.tsx @@ -1,5 +1,4 @@ import * as React from '@theia/core/shared/react'; -import * as ReactDOM from '@theia/core/shared/react-dom'; import { inject, injectable, @@ -25,15 +24,14 @@ export class NotificationsRenderer extends TheiaNotificationsRenderer { } protected override render(): void { - ReactDOM.render( + this.containerRoot.render(
-
, - this.container +
); } } 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 new file mode 100644 index 000000000..4951ba771 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts @@ -0,0 +1,23 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; +import { ArduinoThemes } from '../core/theming'; + +@injectable() +export class MonacoThemingService extends TheiaMonacoThemingService { + override initialize(): void { + super.initialize(); + 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'), + }); + } +} diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts index 22c74728d..1310610a1 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts @@ -1,58 +1,37 @@ -import * as remote from '@theia/core/electron-shared/@electron/remote'; -import { injectable, inject, named } from '@theia/core/shared/inversify'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import URI from '@theia/core/lib/common/uri'; -import { EditorWidget } from '@theia/editor/lib/browser'; -import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; -import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; -import { FocusTracker, Widget } from '@theia/core/lib/browser'; import { DEFAULT_WINDOW_HASH, NewWindowOptions, } from '@theia/core/lib/common/window'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { WorkspaceInput, WorkspaceService as TheiaWorkspaceService, } from '@theia/workspace/lib/browser/workspace-service'; import { - SketchesService, - Sketch, SketchesError, + SketchesService, } from '../../../common/protocol/sketches-service'; -import { FileStat } from '@theia/filesystem/lib/common/files'; import { StartupTask, StartupTaskProvider, } from '../../../electron-common/startup-task'; import { WindowServiceExt } from '../core/window-service-ext'; -import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; @injectable() export class WorkspaceService extends TheiaWorkspaceService { @inject(SketchesService) private readonly sketchService: SketchesService; - @inject(ApplicationServer) - private readonly applicationServer: ApplicationServer; @inject(WindowServiceExt) private readonly windowServiceExt: WindowServiceExt; @inject(ContributionProvider) @named(StartupTaskProvider) private readonly providers: ContributionProvider; - private version?: string; private _workspaceError: Error | undefined; - async onStart(application: FrontendApplication): Promise { - const info = await this.applicationServer.getApplicationInfo(); - this.version = info?.version; - application.shell.onDidChangeCurrentWidget( - this.onCurrentWidgetChange.bind(this) - ); - const newValue = application.shell.currentWidget - ? application.shell.currentWidget - : null; - this.onCurrentWidgetChange({ newValue, oldValue: null }); - } - get workspaceError(): Error | undefined { return this._workspaceError; } @@ -121,58 +100,6 @@ export class WorkspaceService extends TheiaWorkspaceService { } } - /** - * Copied from Theia as-is to be able to pass the original `options` down. - */ - protected override async doOpen( - uri: URI, - options?: WorkspaceInput - ): Promise { - const stat = await this.toFileStat(uri); - if (stat) { - if (!stat.isDirectory && !this.isWorkspaceFile(stat)) { - const message = `Not a valid workspace: ${uri.path.toString()}`; - this.messageService.error(message); - throw new Error(message); - } - // The same window has to be preserved too (instead of opening a new one), if the workspace root is not yet available and we are setting it for the first time. - // Option passed as parameter has the highest priority (for api developers), then the preference, then the default. - await this.roots; - const { preserveWindow } = { - preserveWindow: - this.preferences['workspace.preserveWindow'] || !this.opened, - ...options, - }; - await this.server.setMostRecentlyUsedWorkspace(uri.toString()); - if (preserveWindow) { - this._workspace = stat; - } - this.openWindow(stat, Object.assign(options ?? {}, { preserveWindow })); // Unlike Theia, IDE2 passes the whole `input` downstream and not only { preserveWindow } - return; - } - throw new Error( - 'Invalid workspace root URI. Expected an existing directory or workspace file.' - ); - } - - /** - * Copied from Theia. Can pass the `options` further down the chain. - */ - protected override openWindow(uri: FileStat, options?: WorkspaceInput): void { - const workspacePath = uri.resource.path.toString(); - if (this.shouldPreserveWindow(options)) { - this.reloadWindow(options); // Unlike Theia, IDE2 passes the `input` downstream. - } else { - try { - this.openNewWindow(workspacePath, options); // Unlike Theia, IDE2 passes the `input` downstream. - } catch (error) { - // Fall back to reloading the current window in case the browser has blocked the new window - this._workspace = uri; - this.logger.error(error.toString()).then(() => this.reloadWindow()); - } - } - } - protected override reloadWindow(options?: WorkspaceInput): void { const tasks = this.tasks(options); this.setURLFragment(this._workspace?.resource.path.toString() || ''); @@ -192,6 +119,10 @@ export class WorkspaceService extends TheiaWorkspaceService { ); } + protected override updateTitle(): void { + // NOOP. IDE2 handles the `window.title` updates solely via the customized `WindowTitleUpdater`. + } + private tasks(options?: WorkspaceInput): StartupTask[] { const tasks = this.providers .getContributions() @@ -202,37 +133,4 @@ export class WorkspaceService extends TheiaWorkspaceService { } return tasks; } - - protected onCurrentWidgetChange({ - newValue, - }: FocusTracker.IChangedArgs): void { - if (newValue instanceof EditorWidget) { - const { uri } = newValue.editor; - const currentWindow = remote.getCurrentWindow(); - currentWindow.setRepresentedFilename(uri.path.toString()); - if (Sketch.isSketchFile(uri.toString())) { - this.updateTitle(); - } else { - const title = this.workspaceTitle; - const fileName = this.labelProvider.getName(uri); - document.title = this.formatTitle( - title ? `${title} - ${fileName}` : fileName - ); - } - } else { - this.updateTitle(); - } - } - - protected override formatTitle(title?: string): string { - const version = this.version ? ` ${this.version}` : ''; - const name = `${this.applicationName} ${version}`; - return title ? `${title} | ${name}` : name; - } - - protected get workspaceTitle(): string | undefined { - if (this.workspace) { - return this.labelProvider.getName(this.workspace.resource); - } - } } diff --git a/arduino-ide-extension/src/browser/widgets/arduino-select.tsx b/arduino-ide-extension/src/browser/widgets/arduino-select.tsx index 4ee84e82a..da3ff5f53 100644 --- a/arduino-ide-extension/src/browser/widgets/arduino-select.tsx +++ b/arduino-ide-extension/src/browser/widgets/arduino-select.tsx @@ -1,17 +1,22 @@ import * as React from '@theia/core/shared/react'; import Select from 'react-select'; -import { Styles } from 'react-select/src/styles'; -import { Props } from 'react-select/src/components'; -import { ThemeConfig } from 'react-select/src/theme'; +import type { StylesConfig } from 'react-select/dist/declarations/src/styles'; +import type { ThemeConfig } from 'react-select/dist/declarations/src/theme'; +import type { GroupBase } from 'react-select/dist/declarations/src/types'; +import type { StateManagerProps } from 'react-select/dist/declarations/src/useStateManager'; -export class ArduinoSelect extends Select { - constructor(props: Readonly>) { +export class ArduinoSelect< + Option, + IsMulti extends boolean = false, + Group extends GroupBase