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/.vscode/launch.json b/.vscode/launch.json index 06f959e85..f892c3fc5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,8 @@ "--plugins=local-dir:../plugins", "--hosted-plugin-inspect=9339", "--content-trace", - "--open-devtools" + "--open-devtools", + "--no-ping-timeout", ], "env": { "NODE_ENV": "development" @@ -56,7 +57,8 @@ "--remote-debugging-port=9222", "--no-app-auto-install", "--plugins=local-dir:../plugins", - "--hosted-plugin-inspect=9339" + "--hosted-plugin-inspect=9339", + "--no-ping-timeout", ], "env": { "NODE_ENV": "development" diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index d62235dd7..7d75f7217 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -1,6 +1,6 @@ { "name": "arduino-ide-extension", - "version": "2.0.2", + "version": "2.0.3", "description": "An extension for Theia building the Arduino IDE", "license": "AGPL-3.0-or-later", "scripts": { @@ -21,27 +21,31 @@ }, "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/debug": "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/messages": "1.31.1", + "@theia/monaco": "1.31.1", + "@theia/monaco-editor-core": "1.67.2", + "@theia/navigator": "1.31.1", + "@theia/outline-view": "1.31.1", + "@theia/output": "1.31.1", + "@theia/plugin-ext": "1.31.1", + "@theia/preferences": "1.31.1", + "@theia/scm": "1.31.1", + "@theia/search-in-workspace": "1.31.1", + "@theia/terminal": "1.31.1", + "@theia/typehierarchy": "1.31.1", + "@theia/workspace": "1.31.1", "@tippyjs/react": "^4.2.5", - "@types/atob": "^2.1.2", "@types/auth0-js": "^9.14.0", "@types/btoa": "^1.2.3", "@types/dateformat": "^3.0.1", - "@types/deep-equal": "^1.0.1", "@types/deepmerge": "^2.2.0", "@types/glob": "^7.2.0", "@types/google-protobuf": "^3.7.2", @@ -50,49 +54,50 @@ "@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", + "@vscode/debugprotocol": "^1.51.0", "arduino-serial-plotter-webapp": "0.2.0", "async-mutex": "^0.3.0", - "atob": "^2.1.2", "auth0-js": "^9.14.0", "btoa": "^1.2.1", "classnames": "^2.3.1", "dateformat": "^3.0.3", - "deep-equal": "^2.0.5", "deepmerge": "2.0.1", "electron-updater": "^4.6.5", "fast-safe-stringify": "^2.1.1", "glob": "^7.1.6", "google-protobuf": "^3.20.1", "hash.js": "^1.1.7", - "is-valid-path": "^0.1.1", "js-yaml": "^3.13.1", + "just-diff": "^5.1.1", "jwt-decode": "^3.1.2", "keytar": "7.2.0", "lodash.debounce": "^4.0.8", + "minimatch": "^3.1.2", "ncp": "^2.0.0", "node-fetch": "^2.6.1", "open": "^8.0.6", - "p-queue": "^5.0.0", + "p-debounce": "^2.1.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-perfect-scrollbar": "^1.5.8", + "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", "temp": "^0.9.1", "temp-dir": "^2.0.0", "tree-kill": "^1.2.1", - "upath": "^1.1.2", - "url": "^0.11.0", "which": "^1.3.1" }, "devDependencies": { @@ -101,11 +106,10 @@ "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", "@types/react-window": "^1.8.5", - "@types/sinon": "^10.0.6", - "@types/sinon-chai": "^3.2.6", "chai": "^4.2.0", "chai-string": "^1.5.0", "decompress": "^4.2.0", + "decompress-tarbz2": "^4.1.1", "decompress-targz": "^4.1.1", "decompress-unzip": "^4.0.1", "download": "^7.1.0", @@ -115,9 +119,6 @@ "moment": "^2.24.0", "protoc": "^1.0.4", "shelljs": "^0.8.3", - "sinon": "^12.0.1", - "sinon-chai": "^3.7.0", - "typemoq": "^2.1.0", "uuid": "^3.2.1", "yargs": "^11.1.0" }, diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index bcbd4747f..4227122cb 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -31,7 +31,7 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; -import { ArduinoPreferences } from './arduino-preferences'; +import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences'; import { BoardsServiceProvider } from './boards/boards-service-provider'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { ArduinoMenus } from './menu/arduino-menus'; @@ -58,8 +58,8 @@ export class ArduinoFrontendContribution @inject(CommandRegistry) private readonly commandRegistry: CommandRegistry; - @inject(ArduinoPreferences) - private readonly arduinoPreferences: ArduinoPreferences; + @inject(ElectronWindowPreferences) + private readonly electronWindowPreferences: ElectronWindowPreferences; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; @@ -78,10 +78,10 @@ export class ArduinoFrontendContribution } onStart(app: FrontendApplication): void { - this.arduinoPreferences.onPreferenceChanged((event) => { + this.electronWindowPreferences.onPreferenceChanged((event) => { if (event.newValue !== event.oldValue) { switch (event.preferenceName) { - case 'arduino.window.zoomLevel': + case 'window.zoomLevel': if (typeof event.newValue === 'number') { const webContents = remote.getCurrentWebContents(); webContents.setZoomLevel(event.newValue || 0); @@ -91,11 +91,10 @@ export class ArduinoFrontendContribution } }); this.appStateService.reachedState('ready').then(() => - this.arduinoPreferences.ready.then(() => { + this.electronWindowPreferences.ready.then(() => { const webContents = remote.getCurrentWebContents(); - const zoomLevel = this.arduinoPreferences.get( - 'arduino.window.zoomLevel' - ); + const zoomLevel = + this.electronWindowPreferences.get('window.zoomLevel'); webContents.setZoomLevel(zoomLevel); }) ); 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..22ea313f2 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -1,12 +1,9 @@ import '../../src/browser/style/index.css'; -import { ContainerModule } from '@theia/core/shared/inversify'; +import { Container, 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'; @@ -241,7 +232,6 @@ import { UploadFirmware } from './contributions/upload-firmware'; import { UploadFirmwareDialog, UploadFirmwareDialogProps, - UploadFirmwareDialogWidget, } from './dialogs/firmware-uploader/firmware-uploader-dialog'; import { UploadCertificate } from './contributions/upload-certificate'; @@ -258,7 +248,6 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c import { UserFieldsDialog, UserFieldsDialogProps, - UserFieldsDialogWidget, } from './dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/common'; import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands'; @@ -271,7 +260,6 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl'; import { IDEUpdaterDialog, IDEUpdaterDialogProps, - IDEUpdaterDialogWidget, } from './dialogs/ide-updater/ide-updater-dialog'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; import { MonitorModel } from './monitor-model'; @@ -313,10 +301,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 +321,28 @@ 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'; +import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service'; +import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service'; +import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution'; +import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution'; +import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution'; +import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution'; +import { DebugToolbar } from './theia/debug/debug-toolbar-widget'; +import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; +import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter'; +import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter'; +import { DebugSessionManager } from './theia/debug/debug-session-manager'; +import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; +import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget'; +import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model'; +import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget'; +import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -587,14 +567,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 +810,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 +823,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,12 +907,11 @@ 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), })); - bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope(); bind(UploadFirmwareDialog).toSelf().inSingletonScope(); bind(UploadFirmwareDialogProps).toConstantValue({ title: 'UploadFirmware', @@ -958,13 +922,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { title: 'UploadCertificate', }); - bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope(); bind(IDEUpdaterDialog).toSelf().inSingletonScope(); bind(IDEUpdaterDialogProps).toConstantValue({ title: 'IDEUpdater', }); - bind(UserFieldsDialogWidget).toSelf().inSingletonScope(); bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialogProps).toConstantValue({ title: 'UserFields', @@ -991,4 +953,55 @@ 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); + + // disable type-hierarchy support + // https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be + bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope(); + rebind(TheiaTypeHierarchyServiceProvider).toService( + TypeHierarchyServiceProvider + ); + bind(TypeHierarchyContribution).toSelf().inSingletonScope(); + rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution); + + // patched the debugger for `cortex-debug@1.5.1` + // https://github.com/eclipse-theia/theia/issues/11871 + // https://github.com/eclipse-theia/theia/issues/11879 + // https://github.com/eclipse-theia/theia/issues/11880 + // https://github.com/eclipse-theia/theia/issues/11885 + // https://github.com/eclipse-theia/theia/issues/11886 + // https://github.com/eclipse-theia/theia/issues/11916 + // based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871 + bind(DefaultDebugSessionFactory).toSelf().inSingletonScope(); + rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory); + bind(DebugSessionManager).toSelf().inSingletonScope(); + rebind(TheiaDebugSessionManager).toService(DebugSessionManager); + bind(DebugToolbar).toSelf().inSingletonScope(); + rebind(TheiaDebugToolbar).toService(DebugToolbar); + bind(PluginMenuCommandAdapter).toSelf().inSingletonScope(); + rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter); + bind(WidgetFactory) + .toDynamicValue(({ container }) => ({ + id: DebugWidget.ID, + createWidget: () => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = container; + child.bind(DebugViewModel).toSelf(); + child.bind(DebugToolbar).toSelf(); // patched toolbar + child.bind(DebugSessionWidget).toSelf(); + child.bind(DebugConfigurationWidget).toSelf(); + child.bind(DebugWidget).toSelf(); + return child.get(DebugWidget); + }, + })) + .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index ecd45735d..5adc8b9c2 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -114,11 +114,12 @@ export const ArduinoConfigSchema: PreferenceSchema = { }, 'arduino.window.zoomLevel': { type: 'number', - description: nls.localize( - 'arduino/preferences/window.zoomLevel', - 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.' - ), + description: '', default: 0, + deprecationMessage: nls.localize( + 'arduino/preferences/window.zoomLevel/deprecationMessage', + "Deprecated. Use 'window.zoomLevel' instead." + ), }, 'arduino.ide.updateChannel': { type: 'string', @@ -270,7 +271,6 @@ export interface ArduinoConfiguration { 'arduino.upload.verbose': boolean; 'arduino.upload.verify': boolean; 'arduino.window.autoScale': boolean; - 'arduino.window.zoomLevel': number; 'arduino.ide.updateChannel': UpdateChannel; 'arduino.ide.updateBaseUrl': string; 'arduino.board.certificates': string; diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts index c61852ca5..430821c03 100644 --- a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -202,11 +202,12 @@ export class NewCloudSketch extends Contribution { private treeModelFrom( widget: SketchbookWidget ): CloudSketchbookTreeModel | undefined { - const treeWidget = widget.getTreeWidget(); - if (treeWidget instanceof CloudSketchbookTreeWidget) { - const model = treeWidget.model; - if (model instanceof CloudSketchbookTreeModel) { - return model; + for (const treeWidget of widget.getTreeWidgets()) { + if (treeWidget instanceof CloudSketchbookTreeWidget) { + const model = treeWidget.model; + if (model instanceof CloudSketchbookTreeModel) { + return model; + } } } return undefined; diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts index 99ccbd085..6d4ffa600 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts @@ -20,7 +20,8 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; export class OpenSketchFiles extends SketchContribution { override registerCommands(registry: CommandRegistry): void { registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, { - execute: (uri: URI) => this.openSketchFiles(uri), + execute: (uri: URI, focusMainSketchFile) => + this.openSketchFiles(uri, focusMainSketchFile), }); registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, { execute: ( @@ -33,13 +34,19 @@ export class OpenSketchFiles extends SketchContribution { }); } - private async openSketchFiles(uri: URI): Promise { + private async openSketchFiles( + uri: URI, + focusMainSketchFile = false + ): Promise { try { const sketch = await this.sketchService.loadSketch(uri.toString()); const { mainFileUri, rootFolderFileUris } = sketch; for (const uri of [mainFileUri, ...rootFolderFileUris]) { await this.ensureOpened(uri); } + if (focusMainSketchFile) { + await this.ensureOpened(mainFileUri, true, { mode: 'activate' }); + } if (mainFileUri.endsWith('.pde')) { const message = nls.localize( 'arduino/common/oldFormat', @@ -126,7 +133,7 @@ export class OpenSketchFiles extends SketchContribution { uri: string, forceOpen = false, options?: EditorOpenerOptions - ): Promise { + ): Promise { const widget = this.editorManager.all.find( (widget) => widget.editor.uri.toString() === uri ); @@ -184,23 +191,24 @@ export class OpenSketchFiles extends SketchContribution { // The editor is expected to be attached to the shell and visible in the UI. // The deferred promise does not have to wait for the `editorManager#onCreated` event. // It can resolve earlier. - if (!widget) { + if (widget) { deferred.resolve(editorWidget); } }); const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI - const result = await Promise.race([ + const result: EditorWidget | undefined | 'timeout' = await Promise.race([ deferred.promise, wait(timeout).then(() => { disposables.dispose(); - return 'timeout'; + return 'timeout' as const; }), ]); if (result === 'timeout') { console.warn( `Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}` ); + return undefined; } return result; } diff --git a/arduino-ide-extension/src/browser/contributions/sketch-control.ts b/arduino-ide-extension/src/browser/contributions/sketch-control.ts index 62f2d8ce8..eef34817e 100644 --- a/arduino-ide-extension/src/browser/contributions/sketch-control.ts +++ b/arduino-ide-extension/src/browser/contributions/sketch-control.ts @@ -235,7 +235,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, diff --git a/arduino-ide-extension/src/browser/create/create-uri.ts b/arduino-ide-extension/src/browser/create/create-uri.ts index 658a65ac1..be1a30e9c 100644 --- a/arduino-ide-extension/src/browser/create/create-uri.ts +++ b/arduino-ide-extension/src/browser/create/create-uri.ts @@ -1,4 +1,4 @@ -import { URI as Uri } from 'vscode-uri'; +import { URI as Uri } from '@theia/core/shared/vscode-uri'; import URI from '@theia/core/lib/common/uri'; import { toPosixPath, parentPosix, posix } from './create-paths'; import { Create } from './typings'; diff --git a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx index 448bbf0e4..20d97d4b2 100644 --- a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx @@ -5,10 +5,8 @@ import { postConstruct, } from '@theia/core/shared/inversify'; import { DialogProps } from '@theia/core/lib/browser/dialogs'; -import { AbstractDialog } from '../../theia/dialogs/dialogs'; -import { Widget } from '@theia/core/shared/@phosphor/widgets'; +import { ReactDialog } from '../../theia/dialogs/dialogs'; import { Message } from '@theia/core/shared/@phosphor/messaging'; -import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { AvailableBoard, BoardsServiceProvider, @@ -23,26 +21,30 @@ import { Port } from '../../../common/protocol'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @injectable() -export class UploadFirmwareDialogWidget extends ReactWidget { - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; +export class UploadFirmwareDialogProps extends DialogProps {} +@injectable() +export class UploadFirmwareDialog extends ReactDialog { + @inject(BoardsServiceProvider) + private readonly boardsServiceClient: BoardsServiceProvider; @inject(ArduinoFirmwareUploader) - protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; - + private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; @inject(FrontendApplicationStateService) private readonly appStatusService: FrontendApplicationStateService; - protected updatableFqbns: string[] = []; - protected availableBoards: AvailableBoard[] = []; - protected isOpen = new Object(); - - public busyCallback = (busy: boolean) => { - return; - }; + private updatableFqbns: string[] = []; + private availableBoards: AvailableBoard[] = []; + private isOpen = new Object(); + private busy = false; - constructor() { - super(); + constructor( + @inject(UploadFirmwareDialogProps) + protected override readonly props: UploadFirmwareDialogProps + ) { + super({ title: UploadFirmware.Commands.OPEN.label || '' }); + this.node.id = 'firmware-uploader-dialog-container'; + this.contentNode.classList.add('firmware-uploader-dialog'); + this.acceptButton = undefined; } @postConstruct() @@ -59,79 +61,34 @@ export class UploadFirmwareDialogWidget extends ReactWidget { }); } - protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise { - this.busyCallback(true); - return this.arduinoFirmwareUploader - .flash(firmware, port) - .finally(() => this.busyCallback(false)); - } - - protected override onCloseRequest(msg: Message): void { - super.onCloseRequest(msg); - this.isOpen = new Object(); + get value(): void { + return; } - protected render(): React.ReactNode { + protected override render(): React.ReactNode { return ( -
- - +
+
+ + +
); } -} - -@injectable() -export class UploadFirmwareDialogProps extends DialogProps {} - -@injectable() -export class UploadFirmwareDialog extends AbstractDialog { - @inject(UploadFirmwareDialogWidget) - protected readonly widget: UploadFirmwareDialogWidget; - - private busy = false; - - constructor( - @inject(UploadFirmwareDialogProps) - protected override readonly props: UploadFirmwareDialogProps - ) { - super({ title: UploadFirmware.Commands.OPEN.label || '' }); - this.node.id = 'firmware-uploader-dialog-container'; - this.contentNode.classList.add('firmware-uploader-dialog'); - this.acceptButton = undefined; - } - - get value(): void { - return; - } protected override onAfterAttach(msg: Message): void { - if (this.widget.isAttached) { - Widget.detach(this.widget); - } - Widget.attach(this.widget, this.contentNode); - const firstButton = this.widget.node.querySelector('button'); + const firstButton = this.node.querySelector('button'); firstButton?.focus(); - this.widget.busyCallback = this.busyCallback.bind(this); super.onAfterAttach(msg); this.update(); } - protected override onUpdateRequest(msg: Message): void { - super.onUpdateRequest(msg); - this.widget.update(); - } - - protected override onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - this.widget.activate(); - } - + // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars protected override handleEnter(event: KeyboardEvent): boolean | void { return false; } @@ -140,11 +97,11 @@ export class UploadFirmwareDialog extends AbstractDialog { if (this.busy) { return; } - this.widget.close(); super.close(); + this.isOpen = new Object(); } - busyCallback(busy: boolean): void { + private busyCallback(busy: boolean): void { this.busy = busy; if (busy) { this.closeCrossNode.classList.add('disabled'); @@ -152,4 +109,11 @@ export class UploadFirmwareDialog extends AbstractDialog { this.closeCrossNode.classList.remove('disabled'); } } + + private flashFirmware(firmware: FirmwareInfo, port: Port): Promise { + this.busyCallback(true); + return this.arduinoFirmwareUploader + .flash(firmware, port) + .finally(() => this.busyCallback(false)); + } } 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..ecb4be944 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,6 @@ import { nls } from '@theia/core/lib/common'; -import { shell } from 'electron'; +import { shell } from '@theia/core/electron-shared/@electron/remote'; import * as React from '@theia/core/shared/react'; -import * as ReactDOM from '@theia/core/shared/react-dom'; import ReactMarkdown from 'react-markdown'; import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater'; import ProgressBar from '../../components/ProgressBar'; @@ -28,32 +27,19 @@ export const IDEUpdaterComponent = ({ }, }: IDEUpdaterComponentProps): React.ReactElement => { const { version, releaseNotes } = updateInfo; - const changelogDivRef = - React.useRef() as React.MutableRefObject; + const [changelog, setChangelog] = React.useState(''); React.useEffect(() => { - if (!!releaseNotes && changelogDivRef.current) { - let changelog: string; - if (typeof releaseNotes === 'string') changelog = releaseNotes; - else - changelog = releaseNotes.reduce((acc, item) => { - return item.note ? (acc += `${item.note}\n\n`) : acc; - }, ''); - ReactDOM.render( - ( - href && shell.openExternal(href)} {...props}> - {children} - - ), - }} - > - {changelog} - , - changelogDivRef.current + if (releaseNotes) { + setChangelog( + typeof releaseNotes === 'string' + ? releaseNotes + : releaseNotes.reduce( + (acc, item) => (item.note ? (acc += `${item.note}\n\n`) : acc), + '' + ) ); } - }, [updateInfo]); + }, [releaseNotes, changelog]); const DownloadCompleted: () => React.ReactElement = () => (
@@ -106,9 +92,24 @@ export const IDEUpdaterComponent = ({ version )}
- {releaseNotes && ( + {changelog && (
- )}
diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx index 7dc2d7347..c4f2d940b 100644 --- a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -5,10 +5,8 @@ import { postConstruct, } from '@theia/core/shared/inversify'; import { DialogProps } from '@theia/core/lib/browser/dialogs'; -import { AbstractDialog } from '../../theia/dialogs/dialogs'; -import { Widget } from '@theia/core/shared/@phosphor/widgets'; import { Message } from '@theia/core/shared/@phosphor/messaging'; -import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import { ReactDialog } from '../../theia/dialogs/dialogs'; import { nls } from '@theia/core'; import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component'; import { @@ -22,47 +20,11 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service'; const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software'; -@injectable() -export class IDEUpdaterDialogWidget extends ReactWidget { - private _updateInfo: UpdateInfo; - private _updateProgress: UpdateProgress = {}; - - setUpdateInfo(updateInfo: UpdateInfo): void { - this._updateInfo = updateInfo; - this.update(); - } - - mergeUpdateProgress(updateProgress: UpdateProgress): void { - this._updateProgress = { ...this._updateProgress, ...updateProgress }; - this.update(); - } - - get updateInfo(): UpdateInfo { - return this._updateInfo; - } - - get updateProgress(): UpdateProgress { - return this._updateProgress; - } - - protected render(): React.ReactNode { - return !!this._updateInfo ? ( - - ) : null; - } -} - @injectable() export class IDEUpdaterDialogProps extends DialogProps {} @injectable() -export class IDEUpdaterDialog extends AbstractDialog { - @inject(IDEUpdaterDialogWidget) - private readonly widget: IDEUpdaterDialogWidget; - +export class IDEUpdaterDialog extends ReactDialog { @inject(IDEUpdater) private readonly updater: IDEUpdater; @@ -75,6 +37,9 @@ export class IDEUpdaterDialog extends AbstractDialog { @inject(WindowService) private readonly windowService: WindowService; + private _updateInfo: UpdateInfo | undefined; + private _updateProgress: UpdateProgress = {}; + constructor( @inject(IDEUpdaterDialogProps) protected override readonly props: IDEUpdaterDialogProps @@ -94,26 +59,34 @@ export class IDEUpdaterDialog extends AbstractDialog { protected init(): void { this.updaterClient.onUpdaterDidFail((error) => { this.appendErrorButtons(); - this.widget.mergeUpdateProgress({ error }); + this.mergeUpdateProgress({ error }); }); this.updaterClient.onDownloadProgressDidChange((progressInfo) => { - this.widget.mergeUpdateProgress({ progressInfo }); + this.mergeUpdateProgress({ progressInfo }); }); this.updaterClient.onDownloadDidFinish(() => { this.appendInstallButtons(); - this.widget.mergeUpdateProgress({ downloadFinished: true }); + this.mergeUpdateProgress({ downloadFinished: true }); }); } - get value(): UpdateInfo { - return this.widget.updateInfo; + protected render(): React.ReactNode { + return ( + this.updateInfo && ( + + ) + ); + } + + get value(): UpdateInfo | undefined { + return this.updateInfo; } protected override onAfterAttach(msg: Message): void { - if (this.widget.isAttached) { - Widget.detach(this.widget); - } - Widget.attach(this.widget, this.contentNode); + this.update(); this.appendInitialButtons(); super.onAfterAttach(msg); } @@ -196,15 +169,19 @@ export class IDEUpdaterDialog extends AbstractDialog { } private skipVersion(): void { + if (!this.updateInfo) { + console.warn(`Nothing to skip. No update info is available`); + return; + } this.localStorageService.setData( SKIP_IDE_VERSION, - this.widget.updateInfo.version + this.updateInfo.version ); this.close(); } private startDownload(): void { - this.widget.mergeUpdateProgress({ + this.mergeUpdateProgress({ downloadStarted: true, }); this.clearButtons(); @@ -216,31 +193,48 @@ export class IDEUpdaterDialog extends AbstractDialog { this.close(); } + private set updateInfo(updateInfo: UpdateInfo | undefined) { + this._updateInfo = updateInfo; + this.update(); + } + + private get updateInfo(): UpdateInfo | undefined { + return this._updateInfo; + } + + private get updateProgress(): UpdateProgress { + return this._updateProgress; + } + + private mergeUpdateProgress(updateProgress: UpdateProgress): void { + this._updateProgress = { ...this._updateProgress, ...updateProgress }; + this.update(); + } + override async open( data: UpdateInfo | undefined = undefined ): Promise { if (data && data.version) { - this.widget.mergeUpdateProgress({ + this.mergeUpdateProgress({ progressInfo: undefined, downloadStarted: false, downloadFinished: false, error: undefined, }); - this.widget.setUpdateInfo(data); + this.updateInfo = data; return super.open(); } } protected override onActivateRequest(msg: Message): void { super.onActivateRequest(msg); - this.widget.activate(); + this.update(); } override close(): void { - this.widget.dispose(); if ( - this.widget.updateProgress?.downloadStarted && - !this.widget.updateProgress?.downloadFinished + this.updateProgress?.downloadStarted && + !this.updateProgress?.downloadFinished ) { this.updater.stopDownload(); } 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..73afccb5b 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,17 +25,20 @@ 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 WINDOW_SETTING = 'window'; export const EDITOR_SETTING = 'editor'; export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`; export const AUTO_SAVE_SETTING = `files.autoSave`; export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`; export const ARDUINO_SETTING = 'arduino'; -export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`; +export const ARDUINO_WINDOW_SETTING = `${ARDUINO_SETTING}.window`; export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`; export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`; export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`; -export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`; +export const AUTO_SCALE_SETTING = `${ARDUINO_WINDOW_SETTING}.autoScale`; export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`; export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`; export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`; @@ -53,7 +56,7 @@ export interface Settings { currentLanguage: string; autoScaleInterface: boolean; // `arduino.window.autoScale` - interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751 + interfaceScale: number; // `window.zoomLevel` verboseOnCompile: boolean; // `arduino.compile.verbose` compilerWarnings: CompilerWarnings; // `arduino.compile.warnings` verboseOnUpload: boolean; // `arduino.upload.verbose` @@ -101,6 +104,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 +147,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 +236,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 +253,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 +283,20 @@ 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(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/dialogs/user-fields/user-fields-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx index b95ef21cf..88109cf71 100644 --- a/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx @@ -1,63 +1,18 @@ import * as React from '@theia/core/shared/react'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { - AbstractDialog, - DialogProps, - ReactWidget, -} from '@theia/core/lib/browser'; -import { Widget } from '@theia/core/shared/@phosphor/widgets'; +import { DialogProps } from '@theia/core/lib/browser/dialogs'; import { Message } from '@theia/core/shared/@phosphor/messaging'; import { UploadSketch } from '../../contributions/upload-sketch'; import { UserFieldsComponent } from './user-fields-component'; import { BoardUserField } from '../../../common/protocol'; - -@injectable() -export class UserFieldsDialogWidget extends ReactWidget { - private _currentUserFields: BoardUserField[] = []; - - constructor(private cancel: () => void, private accept: () => Promise) { - super(); - } - - set currentUserFields(userFields: BoardUserField[]) { - this.setUserFields(userFields); - } - - get currentUserFields(): BoardUserField[] { - return this._currentUserFields; - } - - resetUserFieldsValue(): void { - this._currentUserFields = this._currentUserFields.map((field) => { - field.value = ''; - return field; - }); - } - - private setUserFields(userFields: BoardUserField[]): void { - this._currentUserFields = userFields; - } - - protected render(): React.ReactNode { - return ( -
- - - ); - } -} +import { ReactDialog } from '../../theia/dialogs/dialogs'; @injectable() export class UserFieldsDialogProps extends DialogProps {} @injectable() -export class UserFieldsDialog extends AbstractDialog { - protected readonly widget: UserFieldsDialogWidget; +export class UserFieldsDialog extends ReactDialog { + private _currentUserFields: BoardUserField[] = []; constructor( @inject(UserFieldsDialogProps) @@ -69,39 +24,36 @@ export class UserFieldsDialog extends AbstractDialog { this.titleNode.classList.add('user-fields-dialog-title'); this.contentNode.classList.add('user-fields-dialog-content'); this.acceptButton = undefined; - this.widget = new UserFieldsDialogWidget( - this.close.bind(this), - this.accept.bind(this) - ); + } + + get value(): BoardUserField[] { + return this._currentUserFields; } set value(userFields: BoardUserField[]) { - this.widget.currentUserFields = userFields; + this._currentUserFields = userFields; } - get value(): BoardUserField[] { - return this.widget.currentUserFields; + protected override render(): React.ReactNode { + return ( +
+
+ + +
+ ); } protected override onAfterAttach(msg: Message): void { - if (this.widget.isAttached) { - Widget.detach(this.widget); - } - Widget.attach(this.widget, this.contentNode); super.onAfterAttach(msg); this.update(); } - protected override onUpdateRequest(msg: Message): void { - super.onUpdateRequest(msg); - this.widget.update(); - } - - protected override onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - this.widget.activate(); - } - protected override async accept(): Promise { // If the user presses enter and at least // a field is empty don't accept the input @@ -114,8 +66,21 @@ export class UserFieldsDialog extends AbstractDialog { } override close(): void { - this.widget.resetUserFieldsValue(); - this.widget.close(); + this.resetUserFieldsValue(); super.close(); } + + private resetUserFieldsValue(): void { + this.value = this.value.map((field) => { + field.value = ''; + return field; + }); + } + + private readonly doCancel: () => void = () => this.close(); + private readonly doAccept: () => Promise = () => this.accept(); + private readonly doUpdateUserFields: (userFields: BoardUserField[]) => void = + (userFields: BoardUserField[]) => { + this.value = userFields; + }; } diff --git a/arduino-ide-extension/src/browser/icons/loading-dark.svg b/arduino-ide-extension/src/browser/icons/loading-dark.svg new file mode 100644 index 000000000..d886fd06f --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/loading-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/arduino-ide-extension/src/browser/icons/loading-light.svg b/arduino-ide-extension/src/browser/icons/loading-light.svg new file mode 100644 index 000000000..d46f25880 --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/loading-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/arduino-ide-extension/src/browser/local-cache/local-cache-fs-provider.ts b/arduino-ide-extension/src/browser/local-cache/local-cache-fs-provider.ts index 5a9a77c71..ee2aebf99 100644 --- a/arduino-ide-extension/src/browser/local-cache/local-cache-fs-provider.ts +++ b/arduino-ide-extension/src/browser/local-cache/local-cache-fs-provider.ts @@ -1,5 +1,5 @@ import { inject, injectable } from '@theia/core/shared/inversify'; -import { URI as Uri } from 'vscode-uri'; +import { URI as Uri } from '@theia/core/shared/vscode-uri'; import URI from '@theia/core/lib/common/uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { 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/style/index.css b/arduino-ide-extension/src/browser/style/index.css index e46cdcdd7..6aa967304 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -20,6 +20,16 @@ @import './progress-bar.css'; @import './settings-step-input.css'; +/* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */ +/* The SVG icons are still part of Theia (1.31.1) */ +/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */ +body { + --theia-icon-loading: url(../icons/loading-light.svg); +} +body.theia-dark { + --theia-icon-loading: url(../icons/loading-dark.svg); +} + .theia-input.warning:focus { outline-width: 1px; outline-style: solid; @@ -166,3 +176,13 @@ button.theia-button.message-box-dialog-button { outline: 1px dashed var(--theia-focusBorder); outline-offset: -2px; } + +.debug-toolbar .debug-action>div { + font-family: var(--theia-ui-font-family); + font-size: var(--theia-ui-font-size0); + display: flex; + align-items: center; + align-self: center; + justify-content: center; + min-height: inherit; +} 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..29d092017 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 @@ -6,6 +6,8 @@ import { } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommandRegistry } from '@theia/core/lib/common/command'; import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application'; +import { KeybindingRegistry } from '@theia/core/lib/browser'; +import { isOSX } from '@theia/core'; @injectable() export class CommonFrontendContribution extends TheiaCommonFrontendContribution { @@ -22,7 +24,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); } @@ -50,6 +52,36 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution } } + override registerKeybindings(registry: KeybindingRegistry): void { + super.registerKeybindings(registry); + // Workaround for https://github.com/eclipse-theia/theia/issues/11875 + if (isOSX) { + registry.unregisterKeybinding('ctrlcmd+tab'); + registry.unregisterKeybinding('ctrlcmd+alt+d'); + registry.unregisterKeybinding('ctrlcmd+shift+tab'); + registry.unregisterKeybinding('ctrlcmd+alt+a'); + + registry.registerKeybindings( + { + command: CommonCommands.NEXT_TAB.id, + keybinding: 'ctrl+tab', + }, + { + command: CommonCommands.NEXT_TAB.id, + keybinding: 'ctrl+alt+d', + }, + { + command: CommonCommands.PREVIOUS_TAB.id, + keybinding: 'ctrl+shift+tab', + }, + { + command: CommonCommands.PREVIOUS_TAB.id, + keybinding: 'ctrl+alt+a', + } + ); + } + } + override onWillStop(): OnWillStopAction | undefined { // This is NOOP here. All window close and app quit requests are handled in the `Close` contribution. return undefined; diff --git a/arduino-ide-extension/src/browser/theia/core/frontend-application.ts b/arduino-ide-extension/src/browser/theia/core/frontend-application.ts index cb1a96206..4b29df155 100644 --- a/arduino-ide-extension/src/browser/theia/core/frontend-application.ts +++ b/arduino-ide-extension/src/browser/theia/core/frontend-application.ts @@ -1,5 +1,4 @@ import { injectable, inject } from '@theia/core/shared/inversify'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { CommandService } from '@theia/core/lib/common/command'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'; @@ -8,17 +7,16 @@ import { OpenSketchFiles } from '../../contributions/open-sketch-files'; @injectable() export class FrontendApplication extends TheiaFrontendApplication { - @inject(FileService) - protected readonly fileService: FileService; - @inject(WorkspaceService) - protected readonly workspaceService: WorkspaceService; + private readonly workspaceService: WorkspaceService; @inject(CommandService) - protected readonly commandService: CommandService; + private readonly commandService: CommandService; @inject(SketchesService) - protected readonly sketchesService: SketchesService; + private readonly sketchesService: SketchesService; + + private layoutWasRestored = false; protected override async initializeLayout(): Promise { await super.initializeLayout(); @@ -26,10 +24,16 @@ export class FrontendApplication extends TheiaFrontendApplication { for (const root of roots) { await this.commandService.executeCommand( OpenSketchFiles.Commands.OPEN_SKETCH_FILES.id, - root.resource + root.resource, + !this.layoutWasRestored ); this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu } }); } + + protected override async restoreLayout(): Promise { + this.layoutWasRestored = await super.restoreLayout(); + return this.layoutWasRestored; + } } 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..4438d94e8 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/theming.ts @@ -0,0 +1,26 @@ +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', + }; +} + +@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..9e786e262 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/window-title-updater.ts @@ -0,0 +1,87 @@ +import * as remote from '@theia/core/electron-shared/@electron/remote'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types'; +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 { isOSX } from '@theia/core/lib/common/os'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; +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 _previousRepresentedFilename: string | undefined; + + 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 { + if (isOSX) { + this.maybeUpdateRepresentedFilename(widget); + } + // Unlike Theia, IDE2 does not want to show in the window title if the current widget is dirty or not. + // 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 }); + } + + private maybeUpdateRepresentedFilename(widget?: Widget | undefined): void { + if (widget instanceof EditorWidget) { + const { uri } = widget.editor; + const filename = uri.path.toString(); + // Do not necessarily require the current window if not needed. It's a synchronous, blocking call. + if (this._previousRepresentedFilename !== filename) { + const currentWindow = remote.getCurrentWindow(); + currentWindow.setRepresentedFilename(uri.path.toString()); + this._previousRepresentedFilename = filename; + } + } + } +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx b/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx new file mode 100644 index 000000000..c0f691b49 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx @@ -0,0 +1,29 @@ +import * as React from '@theia/core/shared/react'; +import { DebugAction as TheiaDebugAction } from '@theia/debug/lib/browser/view/debug-action'; +import { + codiconArray, + DISABLED_CLASS, +} from '@theia/core/lib/browser/widgets/widget'; + +// customized debug action to show the contributed command's label when there is no icon +export class DebugAction extends TheiaDebugAction { + override render(): React.ReactNode { + const { enabled, label, iconClass } = this.props; + const classNames = ['debug-action', ...codiconArray(iconClass, true)]; + if (enabled === false) { + classNames.push(DISABLED_CLASS); + } + return ( + + {!iconClass || + (iconClass.match(/plugin-icon-\d+/) &&
{label}
)} +
+ ); + } +} 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-contribution.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session-contribution.ts new file mode 100644 index 000000000..d0ba503ef --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-session-contribution.ts @@ -0,0 +1,49 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; +import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution'; +import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { + DebugAdapterPath, + DebugChannel, + ForwardingDebugChannel, +} from '@theia/debug/lib/common/debug-service'; +import { DebugSession } from './debug-session'; + +@injectable() +export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory { + override get( + sessionId: string, + options: DebugConfigurationSessionOptions, + parentSession?: DebugSession + ): DebugSession { + const connection = new DebugSessionConnection( + sessionId, + () => + new Promise((resolve) => + this.connectionProvider.openChannel( + `${DebugAdapterPath}/${sessionId}`, + (wsChannel) => { + resolve(new ForwardingDebugChannel(wsChannel)); + }, + { reconnecting: false } + ) + ), + this.getTraceOutputChannel() + ); + // patched debug session + return new DebugSession( + sessionId, + options, + parentSession, + connection, + this.terminalService, + this.editorManager, + this.breakpoints, + this.labelProvider, + this.messages, + this.fileService, + this.debugContributionProvider, + this.workspaceService + ); + } +} 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 index 6eb2ebdeb..f641a6535 100644 --- a/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts +++ b/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts @@ -1,90 +1,120 @@ -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 type { ContextKey } from '@theia/core/lib/browser/context-key-service'; +import { injectable, postConstruct } from '@theia/core/shared/inversify'; +import { + DebugSession, + DebugState, +} from '@theia/debug/lib/browser/debug-session'; import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; -import { nls } from '@theia/core/lib/common'; +import type { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; + +function debugStateLabel(state: DebugState): string { + switch (state) { + case DebugState.Initializing: + return 'initializing'; + case DebugState.Stopped: + return 'stopped'; + case DebugState.Running: + return 'running'; + default: + return 'inactive'; + } +} @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); + protected debugStateKey: ContextKey; - //#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." + @postConstruct() + protected override init(): void { + this.debugStateKey = this.contextKeyService.createKey( + 'debugState', + debugStateLabel(this.state) + ); + super.init(); + } - if (resolved === null) { - this.debugConfigurationManager.openConfiguration(); - } - return undefined; - } - //#endregion end of cherry-pick + protected override fireDidChange(current: DebugSession | undefined): void { + this.debugTypeKey.set(current?.configuration.type); + this.inDebugModeKey.set(this.inDebugMode); + this.debugStateKey.set(debugStateLabel(this.state)); + this.onDidChangeEmitter.fire(current); + } - // 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; - } - } + protected override async doStart( + sessionId: string, + options: DebugConfigurationSessionOptions + ): Promise { + const parentSession = + options.configuration.parentSession && + this._sessions.get(options.configuration.parentSession.id); + const contrib = this.sessionContributionRegistry.get( + options.configuration.type + ); + const sessionFactory = contrib + ? contrib.debugSessionFactory() + : this.debugSessionFactory; + const session = sessionFactory.get(sessionId, options, parentSession); + this._sessions.set(sessionId, session); - 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.debugTypeKey.set(session.configuration.type); + // this.onDidCreateDebugSessionEmitter.fire(session); // defer the didCreate event after start https://github.com/eclipse-theia/theia/issues/11916 - 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; + let state = DebugState.Inactive; + session.onDidChange(() => { + if (state !== session.state) { + state = session.state; + if (state === DebugState.Stopped) { + this.onDidStopDebugSessionEmitter.fire(session); } } + this.updateCurrentSession(session); + }); + session.onDidChangeBreakpoints((uri) => + this.fireDidChangeBreakpoints({ session, uri }) ); - } - 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); + session.on('terminated', async (event) => { + const restart = event.body && event.body.restart; + if (restart) { + // postDebugTask isn't run in case of auto restart as well as preLaunchTask + this.doRestart(session, !!restart); + } else { + await session.disconnect(false, () => + this.debug.terminateDebugSession(session.id) + ); + await this.runTask( + session.options.workspaceFolderUri, + session.configuration.postDebugTask + ); + } + }); + + // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars + session.on('exited', async (event) => { + await session.disconnect(false, () => + this.debug.terminateDebugSession(session.id) + ); + }); + + session.onDispose(() => this.cleanup(session)); + session + .start() + .then(() => { + this.onDidCreateDebugSessionEmitter.fire(session); // now fire the didCreate event + this.onDidStartDebugSessionEmitter.fire(session); + }) + // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars + .catch((e) => { + session.stop(false, () => { + this.debug.terminateDebugSession(session.id); + }); + }); + session.onDidCustomEvent(({ event, body }) => + this.onDidReceiveDebugSessionCustomEventEmitter.fire({ + event, + body, + session, + }) + ); + return session; } } diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-session.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session.ts new file mode 100644 index 000000000..7db51c2ac --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-session.ts @@ -0,0 +1,231 @@ +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Mutable } from '@theia/core/lib/common/types'; +import { URI } from '@theia/core/lib/common/uri'; +import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session'; +import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint'; +import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint'; +import { + DebugThreadData, + StoppedDetails, +} from '@theia/debug/lib/browser/model/debug-thread'; +import { DebugProtocol } from '@vscode/debugprotocol'; +import { DebugThread } from './debug-thread'; + +export class DebugSession extends TheiaDebugSession { + /** + * The `send('initialize')` request resolves later than `on('initialized')` emits the event. + * Hence, the `configure` would use the empty object `capabilities`. + * Using the empty `capabilities` could result in missing exception breakpoint filters, as + * always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works + * around this timing issue. + * See: https://github.com/eclipse-theia/theia/issues/11886. + */ + protected didReceiveCapabilities = new Deferred(); + + protected override async initialize(): Promise { + const clientName = FrontendApplicationConfigProvider.get().applicationName; + try { + const response = await this.connection.sendRequest('initialize', { + clientID: clientName.toLocaleLowerCase().replace(/ /g, '_'), + clientName, + adapterID: this.configuration.type, + locale: 'en-US', + linesStartAt1: true, + columnsStartAt1: true, + pathFormat: 'path', + supportsVariableType: false, + supportsVariablePaging: false, + supportsRunInTerminalRequest: true, + }); + this.updateCapabilities(response?.body || {}); + this.didReceiveCapabilities.resolve(); + } catch (err) { + this.didReceiveCapabilities.reject(err); + throw err; + } + } + + protected override async configure(): Promise { + await this.didReceiveCapabilities.promise; + return super.configure(); + } + + override async stop(isRestart: boolean, callback: () => void): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const _this = this as any; + if (!_this.isStopping) { + _this.isStopping = true; + if (this.configuration.lifecycleManagedByParent && this.parentSession) { + await this.parentSession.stop(isRestart, callback); + } else { + if (this.canTerminate()) { + const terminated = this.waitFor('terminated', 5000); + try { + await this.connection.sendRequest( + 'terminate', + { restart: isRestart }, + 5000 + ); + await terminated; + } catch (e) { + console.error('Did not receive terminated event in time', e); + } + } else { + const terminateDebuggee = + this.initialized && this.capabilities.supportTerminateDebuggee; + // Related https://github.com/microsoft/vscode/issues/165138 + try { + await this.sendRequest( + 'disconnect', + { restart: isRestart, terminateDebuggee }, + 2000 + ); + } catch (err) { + if ( + 'message' in err && + typeof err.message === 'string' && + err.message.test(err.message) + ) { + // VS Code ignores errors when sending the `disconnect` request. + // Debug adapter might not send the `disconnected` event as a response. + } else { + throw err; + } + } + } + callback(); + } + } + } + + protected override async sendFunctionBreakpoints( + affectedUri: URI + ): Promise { + const all = this.breakpoints + .getFunctionBreakpoints() + .map( + (origin) => + new DebugFunctionBreakpoint(origin, this.asDebugBreakpointOptions()) + ); + const enabled = all.filter((b) => b.enabled); + if (this.capabilities.supportsFunctionBreakpoints) { + try { + const response = await this.sendRequest('setFunctionBreakpoints', { + breakpoints: enabled.map((b) => b.origin.raw), + }); + // Apparently, `body` and `breakpoints` can be missing. + // https://github.com/eclipse-theia/theia/issues/11885 + // https://github.com/microsoft/vscode/blob/80004351ccf0884b58359f7c8c801c91bb827d83/src/vs/workbench/contrib/debug/browser/debugSession.ts#L448-L449 + if (response && response.body) { + response.body.breakpoints.forEach((raw, index) => { + // node debug adapter returns more breakpoints sometimes + if (enabled[index]) { + enabled[index].update({ raw }); + } + }); + } + } catch (error) { + // could be error or promise rejection of DebugProtocol.SetFunctionBreakpoints + if (error instanceof Error) { + console.error(`Error setting breakpoints: ${error.message}`); + } else { + // handle adapters that send failed DebugProtocol.SetFunctionBreakpoints for invalid breakpoints + const genericMessage = + 'Function breakpoint not valid for current debug session'; + const message = error.message ? `${error.message}` : genericMessage; + console.warn( + `Could not handle function breakpoints: ${message}, disabling...` + ); + enabled.forEach((b) => + b.update({ + raw: { + verified: false, + message, + }, + }) + ); + } + } + } + this.setBreakpoints(affectedUri, all); + } + + protected override async sendSourceBreakpoints( + affectedUri: URI, + sourceModified?: boolean + ): Promise { + const source = await this.toSource(affectedUri); + const all = this.breakpoints + .findMarkers({ uri: affectedUri }) + .map( + ({ data }) => + new DebugSourceBreakpoint(data, this.asDebugBreakpointOptions()) + ); + const enabled = all.filter((b) => b.enabled); + try { + const breakpoints = enabled.map(({ origin }) => origin.raw); + const response = await this.sendRequest('setBreakpoints', { + source: source.raw, + sourceModified, + breakpoints, + lines: breakpoints.map(({ line }) => line), + }); + response.body.breakpoints.forEach((raw, index) => { + // node debug adapter returns more breakpoints sometimes + if (enabled[index]) { + enabled[index].update({ raw }); + } + }); + } catch (error) { + // could be error or promise rejection of DebugProtocol.SetBreakpointsResponse + if (error instanceof Error) { + console.error(`Error setting breakpoints: ${error.message}`); + } else { + // handle adapters that send failed DebugProtocol.SetBreakpointsResponse for invalid breakpoints + const genericMessage = 'Breakpoint not valid for current debug session'; + const message = error.message ? `${error.message}` : genericMessage; + console.warn( + `Could not handle breakpoints for ${affectedUri}: ${message}, disabling...` + ); + enabled.forEach((b) => + b.update({ + raw: { + verified: false, + message, + }, + }) + ); + } + } + this.setSourceBreakpoints(affectedUri, all); + } + + protected override doUpdateThreads( + threads: DebugProtocol.Thread[], + stoppedDetails?: StoppedDetails + ): void { + const existing = this._threads; + this._threads = new Map(); + for (const raw of threads) { + const id = raw.id; + const thread = existing.get(id) || new DebugThread(this); // patched debug thread + this._threads.set(id, thread); + const data: Partial> = { raw }; + if (stoppedDetails) { + if (stoppedDetails.threadId === id) { + data.stoppedDetails = stoppedDetails; + } else if (stoppedDetails.allThreadsStopped) { + data.stoppedDetails = { + // When a debug adapter notifies us that all threads are stopped, + // we do not know why the others are stopped, so we should default + // to something generic. + reason: '', + }; + } + } + thread.update(data); + } + this.updateCurrentThread(stoppedDetails); + } +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts b/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts new file mode 100644 index 000000000..c52e238ae --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts @@ -0,0 +1,32 @@ +import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler'; +import { Range } from '@theia/core/shared/vscode-languageserver-types'; +import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame'; +import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; + +export class DebugStackFrame extends TheiaDebugStackFrame { + override async open( + options: WidgetOpenerOptions = { + mode: 'reveal', + } + ): Promise { + if (!this.source) { + return undefined; + } + const { line, column, endLine, endColumn, source } = this.raw; + if (!source) { + return undefined; + } + // create selection based on VS Code + // https://github.com/eclipse-theia/theia/issues/11880 + const selection = Range.create( + line, + column, + endLine || line, + endColumn || column + ); + this.source.open({ + ...options, + selection, + }); + } +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts b/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts new file mode 100644 index 000000000..bb0d3313c --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts @@ -0,0 +1,22 @@ +import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame'; +import { DebugThread as TheiaDebugThread } from '@theia/debug/lib/browser/model/debug-thread'; +import { DebugProtocol } from '@vscode/debugprotocol'; +import { DebugStackFrame } from './debug-stack-frame'; + +export class DebugThread extends TheiaDebugThread { + protected override doUpdateFrames( + frames: DebugProtocol.StackFrame[] + ): TheiaDebugStackFrame[] { + const result = new Set(); + for (const raw of frames) { + const id = raw.id; + const frame = + this._frames.get(id) || new DebugStackFrame(this, this.session); // patched debug stack frame + this._frames.set(id, frame); + frame.update({ raw }); + result.add(frame); + } + this.updateCurrentFrame(); + return [...result.values()]; + } +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx b/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx new file mode 100644 index 000000000..bc6e135e8 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx @@ -0,0 +1,85 @@ +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { + ActionMenuNode, + CompositeMenuNode, + MenuModelRegistry, +} from '@theia/core/lib/common/menu'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { DebugState } from '@theia/debug/lib/browser/debug-session'; +import { DebugAction } from './debug-action'; +import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; + +@injectable() +export class DebugToolbar extends TheiaDebugToolbar { + @inject(CommandRegistry) private readonly commandRegistry: CommandRegistry; + @inject(MenuModelRegistry) + private readonly menuModelRegistry: MenuModelRegistry; + @inject(ContextKeyService) + private readonly contextKeyService: ContextKeyService; + + protected override render(): React.ReactNode { + const { state } = this.model; + return ( + + {this.renderContributedCommands()} + {this.renderContinue()} + + + + + {this.renderStart()} + + ); + } + + private renderContributedCommands(): React.ReactNode { + return this.menuModelRegistry + .getMenu(TheiaDebugToolbar.MENU) + .children.filter((node) => node instanceof CompositeMenuNode) + .map((node) => (node as CompositeMenuNode).children) + .reduce((acc, curr) => acc.concat(curr), []) + .filter((node) => node instanceof ActionMenuNode) + .map((node) => this.debugAction(node as ActionMenuNode)); + } + + private debugAction(node: ActionMenuNode): React.ReactNode { + const { label, command, when, icon: iconClass = '' } = node; + const run = () => this.commandRegistry.executeCommand(command); + const enabled = when ? this.contextKeyService.match(when) : true; + return ( + enabled && ( + + ) + ); + } +} diff --git a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.ts b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.ts deleted file mode 100644 index b93131c7f..000000000 --- a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; - -import { - AbstractDialog as TheiaAbstractDialog, - codiconArray, - DialogProps, -} from '@theia/core/lib/browser'; - -@injectable() -export abstract class AbstractDialog extends TheiaAbstractDialog { - constructor(@inject(DialogProps) protected override readonly props: DialogProps) { - super(props); - - this.closeCrossNode.classList.remove(...codiconArray('close')); - this.closeCrossNode.classList.add('fa', 'fa-close'); - } -} diff --git a/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx new file mode 100644 index 000000000..57eef5639 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/dialogs/dialogs.tsx @@ -0,0 +1,63 @@ +import { + AbstractDialog as TheiaAbstractDialog, + DialogProps, +} from '@theia/core/lib/browser/dialogs'; +import { ReactDialog as TheiaReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog'; +import { codiconArray, Message } from '@theia/core/lib/browser/widgets/widget'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { createRoot } from '@theia/core/shared/react-dom/client'; + +@injectable() +export abstract class AbstractDialog extends TheiaAbstractDialog { + constructor( + @inject(DialogProps) protected override readonly props: DialogProps + ) { + super(props); + + this.closeCrossNode.classList.remove(...codiconArray('close')); + this.closeCrossNode.classList.add('fa', 'fa-close'); + } +} + +@injectable() +export abstract class ReactDialog extends TheiaReactDialog { + protected override onUpdateRequest(msg: Message): void { + // This is tricky to bypass the default Theia code. + // Otherwise, there is a warning when opening the dialog for the second time. + // You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it. + const disposables = new DisposableCollection(); + if (!this.isMounted) { + // toggle the `isMounted` logic for the time being of the super call so that the `createRoot` does not run + this.isMounted = true; + disposables.push(Disposable.create(() => (this.isMounted = false))); + } + + // Always unset the `contentNodeRoot` so there is no double update when calling super. + const restoreContentNodeRoot = this.contentNodeRoot; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.contentNodeRoot as any) = undefined; + disposables.push( + Disposable.create(() => (this.contentNodeRoot = restoreContentNodeRoot)) + ); + + try { + super.onUpdateRequest(msg); + } finally { + disposables.dispose(); + } + + // Use the patched rendering. + if (!this.isMounted) { + this.contentNodeRoot = createRoot(this.contentNode); + // Resetting the prop is missing from the Theia code. + // https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/browser/dialogs/react-dialog.tsx#L41-L47 + this.isMounted = true; + } + this.contentNodeRoot?.render(<>{this.render()}); + } +} 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/markers/problem-manager.ts b/arduino-ide-extension/src/browser/theia/markers/problem-manager.ts index b260bdab3..b218dde3e 100644 --- a/arduino-ide-extension/src/browser/theia/markers/problem-manager.ts +++ b/arduino-ide-extension/src/browser/theia/markers/problem-manager.ts @@ -3,7 +3,7 @@ import { injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { Diagnostic } from 'vscode-languageserver-types'; +import { Diagnostic } from '@theia/core/shared/vscode-languageserver-types'; import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core'; import { Marker } from '@theia/markers/lib/common/marker'; 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/plugin-ext/debug-main.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/debug-main.ts new file mode 100644 index 000000000..0845d6196 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/debug-main.ts @@ -0,0 +1,66 @@ +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; +import { DebugMainImpl as TheiaDebugMainImpl } from '@theia/plugin-ext/lib/main/browser/debug/debug-main'; +import { PluginDebugAdapterContribution } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-adapter-contribution'; +import { PluginDebugSessionFactory } from './plugin-debug-session-factory'; + +export class DebugMainImpl extends TheiaDebugMainImpl { + override async $registerDebuggerContribution( + description: DebuggerDescription + ): Promise { + const debugType = description.type; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const _this = this; + const terminalOptionsExt = await _this.debugExt.$getTerminalCreationOptions( + debugType + ); + + if (_this.toDispose.disposed) { + return; + } + + const debugSessionFactory = new PluginDebugSessionFactory( + _this.terminalService, + _this.editorManager, + _this.breakpointsManager, + _this.labelProvider, + _this.messages, + _this.outputChannelManager, + _this.debugPreferences, + async (sessionId: string) => { + const connection = await _this.connectionMain.ensureConnection( + sessionId + ); + return connection; + }, + _this.fileService, + terminalOptionsExt, + _this.debugContributionProvider, + _this.workspaceService + ); + + const toDispose = new DisposableCollection( + Disposable.create(() => _this.debuggerContributions.delete(debugType)) + ); + _this.debuggerContributions.set(debugType, toDispose); + toDispose.pushAll([ + _this.pluginDebugService.registerDebugAdapterContribution( + new PluginDebugAdapterContribution( + description, + _this.debugExt, + _this.pluginService + ) + ), + _this.sessionContributionRegistrator.registerDebugSessionContribution({ + debugType: description.type, + debugSessionFactory: () => debugSessionFactory, + }), + ]); + _this.toDispose.push( + Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType)) + ); + } +} diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts index 4ef4b1f55..666a6eedc 100644 --- a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts @@ -1,7 +1,17 @@ import { Emitter, Event, JsonRpcProxy } from '@theia/core'; import { injectable, interfaces } from '@theia/core/shared/inversify'; import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol'; -import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; +import { + HostedPluginSupport as TheiaHostedPluginSupport, + PluginHost, +} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { PluginWorker } from '@theia/plugin-ext/lib/hosted/browser/plugin-worker'; +import { setUpPluginApi } from '@theia/plugin-ext/lib/main/browser/main-context'; +import { PLUGIN_RPC_CONTEXT } from '@theia/plugin-ext/lib/common/plugin-api-rpc'; +import { DebugMainImpl } from './debug-main'; +import { ConnectionImpl } from '@theia/plugin-ext/lib/common/connection'; + @injectable() export class HostedPluginSupport extends TheiaHostedPluginSupport { private readonly onDidLoadEmitter = new Emitter(); @@ -31,4 +41,26 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (this as any).server; } + + // to patch the VS Code extension based debugger + // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars + protected override initRpc(host: PluginHost, pluginId: string): RPCProtocol { + const rpc = + host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(host); + setUpPluginApi(rpc, this.container); + this.patchDebugMain(rpc); + this.mainPluginApiProviders + .getContributions() + .forEach((p) => p.initialize(rpc, this.container)); + return rpc; + } + + private patchDebugMain(rpc: RPCProtocol): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const connectionMain = (rpc as any).locals.get( + PLUGIN_RPC_CONTEXT.CONNECTION_MAIN.id + ) as ConnectionImpl; + const debugMain = new DebugMainImpl(rpc, connectionMain, this.container); + rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain); + } } diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts new file mode 100644 index 000000000..a84275e1a --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts @@ -0,0 +1,37 @@ +import { DebugSession } from '@theia/debug/lib/browser/debug-session'; +import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; +import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { PluginDebugSessionFactory as TheiaPluginDebugSessionFactory } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-session-factory'; +import { PluginDebugSession } from './plugin-debug-session'; + +export class PluginDebugSessionFactory extends TheiaPluginDebugSessionFactory { + override get( + sessionId: string, + options: DebugConfigurationSessionOptions, + parentSession?: DebugSession + ): DebugSession { + const connection = new DebugSessionConnection( + sessionId, + this.connectionFactory, + this.getTraceOutputChannel() + ); + + return new PluginDebugSession( + sessionId, + options, + parentSession, + connection, + this.terminalService, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.editorManager as any, + this.breakpoints, + this.labelProvider, + this.messages, + this.fileService, + this.terminalOptionsExt, + this.debugContributionProvider, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.workspaceService as any + ); + } +} diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts new file mode 100644 index 000000000..28b1de7ef --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts @@ -0,0 +1,62 @@ +import { ContributionProvider, MessageClient } from '@theia/core'; +import { LabelProvider } from '@theia/core/lib/browser'; +import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager'; +import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution'; +import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session'; +import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; +import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { TerminalOptionsExt } from '@theia/plugin-ext'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { + TerminalWidget, + TerminalWidgetOptions, +} from '@theia/terminal/lib/browser/base/terminal-widget'; +import { DebugSession } from '../debug/debug-session'; +import { EditorManager } from '../editor/editor-manager'; +import { WorkspaceService } from '../workspace/workspace-service'; + +// This class extends the patched debug session, and not the default debug session from Theia +export class PluginDebugSession extends DebugSession { + constructor( + override readonly id: string, + override readonly options: DebugConfigurationSessionOptions, + override readonly parentSession: TheiaDebugSession | undefined, + protected override readonly connection: DebugSessionConnection, + protected override readonly terminalServer: TerminalService, + protected override readonly editorManager: EditorManager, + protected override readonly breakpoints: BreakpointManager, + protected override readonly labelProvider: LabelProvider, + protected override readonly messages: MessageClient, + protected override readonly fileService: FileService, + protected readonly terminalOptionsExt: TerminalOptionsExt | undefined, + protected override readonly debugContributionProvider: ContributionProvider, + protected override readonly workspaceService: WorkspaceService + ) { + super( + id, + options, + parentSession, + connection, + terminalServer, + editorManager, + breakpoints, + labelProvider, + messages, + fileService, + debugContributionProvider, + workspaceService + ); + } + + protected override async doCreateTerminal( + terminalWidgetOptions: TerminalWidgetOptions + ): Promise { + terminalWidgetOptions = Object.assign( + {}, + terminalWidgetOptions, + this.terminalOptionsExt + ); + return super.doCreateTerminal(terminalWidgetOptions); + } +} diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts new file mode 100644 index 000000000..156bbb2cb --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts @@ -0,0 +1,73 @@ +import { MenuPath } from '@theia/core'; +import { TAB_BAR_TOOLBAR_CONTEXT_MENU } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { injectable, postConstruct } from '@theia/core/shared/inversify'; +import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; +import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variables-widget'; +import { + ArgumentAdapter, + PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter, +} from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter'; +import { + codeToTheiaMappings, + ContributionPoint, +} from '@theia/plugin-ext/lib/main/browser/menus/vscode-theia-menu-mappings'; + +function patch( + toPatch: typeof codeToTheiaMappings, + key: string, + value: MenuPath[] +): void { + const loose = toPatch as Map; + if (!loose.has(key)) { + loose.set(key, value); + } +} +// mappings is a const and cannot be customized with DI +patch(codeToTheiaMappings, 'debug/variables/context', [ + DebugVariablesWidget.CONTEXT_MENU, +]); +patch(codeToTheiaMappings, 'debug/toolBar', [DebugToolBar.MENU]); + +@injectable() +export class PluginMenuCommandAdapter extends TheiaPluginMenuCommandAdapter { + @postConstruct() + protected override init(): void { + const toCommentArgs: ArgumentAdapter = (...args) => + this.toCommentArgs(...args); + const firstArgOnly: ArgumentAdapter = (...args) => [args[0]]; + const noArgs: ArgumentAdapter = () => []; + const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args); + const selectedResource = () => this.getSelectedResources(); + const widgetURI: ArgumentAdapter = (widget) => + this.codeEditorUtil.is(widget) + ? [this.codeEditorUtil.getResourceUri(widget)] + : []; + (>[ + ['comments/comment/context', toCommentArgs], + ['comments/comment/title', toCommentArgs], + ['comments/commentThread/context', toCommentArgs], + ['debug/callstack/context', firstArgOnly], + ['debug/variables/context', firstArgOnly], + ['debug/toolBar', noArgs], + ['editor/context', selectedResource], + ['editor/title', widgetURI], + ['editor/title/context', selectedResource], + ['explorer/context', selectedResource], + ['scm/resourceFolder/context', toScmArgs], + ['scm/resourceGroup/context', toScmArgs], + ['scm/resourceState/context', toScmArgs], + ['scm/title', () => this.toScmArg(this.scmService.selectedRepository)], + ['timeline/item/context', (...args) => this.toTimelineArgs(...args)], + ['view/item/context', (...args) => this.toTreeArgs(...args)], + ['view/title', noArgs], + ]).forEach(([contributionPoint, adapter]) => { + if (adapter) { + const paths = codeToTheiaMappings.get(contributionPoint); + if (paths) { + paths.forEach((path) => this.addArgumentAdapter(path, adapter)); + } + } + }); + this.addArgumentAdapter(TAB_BAR_TOOLBAR_CONTEXT_MENU, widgetURI); + } +} diff --git a/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-contribution.ts b/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-contribution.ts new file mode 100644 index 000000000..5afd1e8cf --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-contribution.ts @@ -0,0 +1,32 @@ +import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { MenuModelRegistry } from '@theia/core/lib/common/menu'; +import { injectable } from '@theia/core/shared/inversify'; +import { + TypeHierarchyCommands, + TypeHierarchyContribution as TheiaTypeHierarchyContribution, +} from '@theia/typehierarchy/lib/browser/typehierarchy-contribution'; + +@injectable() +export class TypeHierarchyContribution extends TheiaTypeHierarchyContribution { + protected override init(): void { + // NOOP + } + + override registerCommands(registry: CommandRegistry): void { + super.registerCommands(registry); + registry.unregisterCommand(TypeHierarchyCommands.OPEN_SUBTYPE.id); + registry.unregisterCommand(TypeHierarchyCommands.OPEN_SUPERTYPE.id); + } + + override registerMenus(registry: MenuModelRegistry): void { + super.registerMenus(registry); + registry.unregisterMenuAction(TypeHierarchyCommands.OPEN_SUBTYPE.id); + registry.unregisterMenuAction(TypeHierarchyCommands.OPEN_SUPERTYPE.id); + } + + override registerKeybindings(registry: KeybindingRegistry): void { + super.registerKeybindings(registry); + registry.unregisterKeybinding(TypeHierarchyCommands.OPEN_SUBTYPE.id); + } +} diff --git a/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-service.ts b/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-service.ts new file mode 100644 index 000000000..1062eea89 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/typehierarchy/type-hierarchy-service.ts @@ -0,0 +1,9 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service'; + +@injectable() +export class TypeHierarchyServiceProvider extends TheiaTypeHierarchyServiceProvider { + override init(): void { + // NOOP + } +} 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