chore: updated to Theia 1.37.0

- Updated `@theia/*` to `1.37.0`.
 - Fixed all `yarn audit` security vulnerabilities.
 - Updated to `electron@23.2.4`:
   - `contextIsolation` is `true`,
   - `nodeIntegration` is `false`, and the
   - `webpack` target is moved from `electron-renderer` to `web`.
 - Updated to `typescript@4.9.3`.
 - Updated the `eslint` plugins.
 - Added the new `Light High Contrast` theme to the IDE2.
 - High contrast themes use Theia APIs for style adjustments.
 - Support for ESM modules: `"moduleResolution": "node16"`.
 - Node.js >= 16.14 is required.
 - VISX langage packs were bumped to `1.70.0`.
 - Removed undesired editor context menu items. (Closes #1394)

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-03-13 14:40:31 +01:00 committed by Akos Kitta
parent 964ea3bc0c
commit 192aac5a81
167 changed files with 6173 additions and 6467 deletions

View File

@ -16,6 +16,7 @@ module.exports = {
'docs/*', 'docs/*',
'scripts/*', 'scripts/*',
'electron-app/*', 'electron-app/*',
'!electron-app/webpack.config.js',
'plugins/*', 'plugins/*',
'arduino-ide-extension/src/node/cli-protocol', 'arduino-ide-extension/src/node/cli-protocol',
], ],

View File

@ -57,10 +57,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js 16.x - name: Install Node.js 16.14
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16.x' node-version: '16.14'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Python 3.x - name: Install Python 3.x

View File

@ -29,10 +29,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js 16.x - name: Install Node.js 16.14
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16.x' node-version: '16.14'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go

View File

@ -16,10 +16,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js 16.x - name: Install Node.js 16.14
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16.x' node-version: '16.14'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go

View File

@ -16,10 +16,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js 16.x - name: Install Node.js 16.14
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16.x' node-version: '16.14'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ build/
arduino-ide-extension/Examples/ arduino-ide-extension/Examples/
!electron/build/ !electron/build/
src-gen/ src-gen/
webpack.config.js electron/build/webpack.config.js
gen-webpack.config.js gen-webpack.config.js
.DS_Store .DS_Store
# switching from `electron` to `browser` in dev mode. # switching from `electron` to `browser` in dev mode.

View File

@ -9,7 +9,7 @@
"compose-changelog": "node ./scripts/compose-changelog.js", "compose-changelog": "node ./scripts/compose-changelog.js",
"download-cli": "node ./scripts/download-cli.js", "download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js", "download-fwuploader": "node ./scripts/download-fwuploader.js",
"copy-i18n": "npx ncp ../i18n ./build/i18n", "copy-i18n": "ncp ../i18n ./build/i18n",
"download-ls": "node ./scripts/download-ls.js", "download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js", "download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js", "generate-protocol": "node ./scripts/generate-protocol.js",
@ -21,28 +21,28 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\"" "test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.6.7", "@grpc/grpc-js": "^1.8.14",
"@theia/application-package": "1.31.1", "@theia/application-package": "1.37.0",
"@theia/core": "1.31.1", "@theia/core": "1.37.0",
"@theia/debug": "1.31.1", "@theia/debug": "1.37.0",
"@theia/editor": "1.31.1", "@theia/editor": "1.37.0",
"@theia/electron": "1.31.1", "@theia/electron": "1.37.0",
"@theia/filesystem": "1.31.1", "@theia/filesystem": "1.37.0",
"@theia/keymaps": "1.31.1", "@theia/keymaps": "1.37.0",
"@theia/markers": "1.31.1", "@theia/markers": "1.37.0",
"@theia/messages": "1.31.1", "@theia/messages": "1.37.0",
"@theia/monaco": "1.31.1", "@theia/monaco": "1.37.0",
"@theia/monaco-editor-core": "1.67.2", "@theia/monaco-editor-core": "1.72.3",
"@theia/navigator": "1.31.1", "@theia/navigator": "1.37.0",
"@theia/outline-view": "1.31.1", "@theia/outline-view": "1.37.0",
"@theia/output": "1.31.1", "@theia/output": "1.37.0",
"@theia/plugin-ext": "1.31.1", "@theia/plugin-ext": "1.37.0",
"@theia/preferences": "1.31.1", "@theia/preferences": "1.37.0",
"@theia/scm": "1.31.1", "@theia/scm": "1.37.0",
"@theia/search-in-workspace": "1.31.1", "@theia/search-in-workspace": "1.37.0",
"@theia/terminal": "1.31.1", "@theia/terminal": "1.37.0",
"@theia/typehierarchy": "1.31.1", "@theia/typehierarchy": "1.37.0",
"@theia/workspace": "1.31.1", "@theia/workspace": "1.37.0",
"@tippyjs/react": "^4.2.5", "@tippyjs/react": "^4.2.5",
"@types/auth0-js": "^9.14.0", "@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3", "@types/btoa": "^1.2.3",
@ -51,6 +51,7 @@
"@types/glob": "^7.2.0", "@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2", "@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2", "@types/js-yaml": "^3.12.2",
"@types/jsdom": "^21.1.1",
"@types/keytar": "^4.4.0", "@types/keytar": "^4.4.0",
"@types/lodash.debounce": "^4.0.6", "@types/lodash.debounce": "^4.0.6",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
@ -65,7 +66,7 @@
"auth0-js": "^9.14.0", "auth0-js": "^9.14.0",
"btoa": "^1.2.1", "btoa": "^1.2.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cpy": "^8.1.2", "cpy": "^10.0.0",
"cross-fetch": "^3.1.5", "cross-fetch": "^3.1.5",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
@ -76,8 +77,9 @@
"glob": "^7.1.6", "glob": "^7.1.6",
"google-protobuf": "^3.20.1", "google-protobuf": "^3.20.1",
"hash.js": "^1.1.7", "hash.js": "^1.1.7",
"is-online": "^9.0.1", "is-online": "^10.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsdom": "^21.1.1",
"jsonc-parser": "^2.2.0", "jsonc-parser": "^2.2.0",
"just-diff": "^5.1.1", "just-diff": "^5.1.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
@ -88,6 +90,7 @@
"open": "^8.0.6", "open": "^8.0.6",
"p-debounce": "^2.1.0", "p-debounce": "^2.1.0",
"p-queue": "^2.4.2", "p-queue": "^2.4.2",
"process": "^0.11.10",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react-disable": "^0.1.1", "react-disable": "^0.1.1",
@ -101,6 +104,7 @@
"temp": "^0.9.1", "temp": "^0.9.1",
"temp-dir": "^2.0.0", "temp-dir": "^2.0.0",
"tree-kill": "^1.2.1", "tree-kill": "^1.2.1",
"util": "^0.12.5",
"which": "^1.3.1" "which": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
@ -147,6 +151,9 @@
"examples" "examples"
], ],
"theiaExtensions": [ "theiaExtensions": [
{
"preload": "lib/electron-browser/preload"
},
{ {
"backend": "lib/node/arduino-ide-backend-module", "backend": "lib/node/arduino-ide-backend-module",
"frontend": "lib/browser/arduino-ide-frontend-module" "frontend": "lib/browser/arduino-ide-frontend-module"
@ -157,6 +164,9 @@
{ {
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module" "frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
}, },
{
"frontendElectron": "lib/electron-browser/electron-arduino-module"
},
{ {
"electronMain": "lib/electron-main/arduino-electron-main-module" "electronMain": "lib/electron-main/arduino-electron-main-module"
} }

View File

@ -0,0 +1,13 @@
import type { Disposable } from '@theia/core/lib/common/disposable';
import type { StartupTasks } from '../electron-common/startup-task';
import type { Sketch } from './contributions/contribution';
export const AppService = Symbol('AppService');
export interface AppService {
quit(): void;
version(): Promise<string>;
registerStartupTasksHandler(
handler: (tasks: StartupTasks) => void
): Disposable;
scheduleDeletion(sketch: Sketch): void; // TODO: find a better place
}

View File

@ -1,41 +1,47 @@
import * as remote from '@theia/core/electron-shared/@electron/remote'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
ColorTheme,
CssStyleCollector,
StylingParticipant,
} from '@theia/core/lib/browser/styling-service';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
} from '@theia/core/lib/common/menu';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { isHighContrast } from '@theia/core/lib/common/theme';
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
import { import {
inject, inject,
injectable, injectable,
postConstruct, postConstruct,
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import { import { EditorCommands } from '@theia/editor/lib/browser/editor-command';
MAIN_MENU_BAR, import { EditorMainMenu } from '@theia/editor/lib/browser/editor-menu';
MenuContribution,
MenuModelRegistry,
} from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { nls } from '@theia/core/lib/common';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider'; import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { ArduinoMenus } from './menu/arduino-menus'; import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution'; import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution'; import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
@injectable() @injectable()
export class ArduinoFrontendContribution export class ArduinoFrontendContribution
@ -44,7 +50,8 @@ export class ArduinoFrontendContribution
TabBarToolbarContribution, TabBarToolbarContribution,
CommandContribution, CommandContribution,
MenuContribution, MenuContribution,
ColorContribution ColorContribution,
StylingParticipant
{ {
@inject(MessageService) @inject(MessageService)
private readonly messageService: MessageService; private readonly messageService: MessageService;
@ -80,8 +87,7 @@ export class ArduinoFrontendContribution
switch (event.preferenceName) { switch (event.preferenceName) {
case 'window.zoomLevel': case 'window.zoomLevel':
if (typeof event.newValue === 'number') { if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents(); window.electronTheiaCore.setZoomLevel(event.newValue || 0);
webContents.setZoomLevel(event.newValue || 0);
} }
break; break;
} }
@ -89,10 +95,9 @@ export class ArduinoFrontendContribution
}); });
this.appStateService.reachedState('ready').then(() => this.appStateService.reachedState('ready').then(() =>
this.electronWindowPreferences.ready.then(() => { this.electronWindowPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = const zoomLevel =
this.electronWindowPreferences.get('window.zoomLevel'); this.electronWindowPreferences.get('window.zoomLevel');
webContents.setZoomLevel(zoomLevel); window.electronTheiaCore.setZoomLevel(zoomLevel);
}) })
); );
} }
@ -168,7 +173,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'button.background', dark: 'button.background',
light: 'button.background', light: 'button.background',
hc: 'activityBar.inactiveForeground', hcDark: 'activityBar.inactiveForeground',
hcLight: 'activityBar.inactiveForeground',
}, },
description: description:
'Background color of the toolbar items. Such as Upload, Verify, etc.', 'Background color of the toolbar items. Such as Upload, Verify, etc.',
@ -178,7 +184,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'button.hoverBackground', dark: 'button.hoverBackground',
light: 'button.hoverBackground', light: 'button.hoverBackground',
hc: 'button.background', hcDark: 'button.background',
hcLight: 'button.background',
}, },
description: description:
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.', 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
@ -188,7 +195,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'secondaryButton.foreground', dark: 'secondaryButton.foreground',
light: 'button.foreground', light: 'button.foreground',
hc: 'activityBar.inactiveForeground', hcDark: 'activityBar.inactiveForeground',
hcLight: 'activityBar.inactiveForeground',
}, },
description: description:
'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter', 'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter',
@ -198,7 +206,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'secondaryButton.hoverBackground', dark: 'secondaryButton.hoverBackground',
light: 'button.hoverBackground', light: 'button.hoverBackground',
hc: 'textLink.foreground', hcDark: 'textLink.foreground',
hcLight: 'textLink.foreground',
}, },
description: description:
'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"', 'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"',
@ -208,7 +217,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'editor.selectionBackground', dark: 'editor.selectionBackground',
light: 'editor.selectionBackground', light: 'editor.selectionBackground',
hc: 'textPreformat.foreground', hcDark: 'textPreformat.foreground',
hcLight: 'textPreformat.foreground',
}, },
description: description:
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)', 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
@ -218,37 +228,38 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'dropdown.border', dark: 'dropdown.border',
light: 'dropdown.border', light: 'dropdown.border',
hc: 'dropdown.border', hcDark: 'dropdown.border',
hcLight: 'dropdown.border',
}, },
description: 'Border color of the Board Selector.', description: 'Border color of the Board Selector.',
}, },
{ {
id: 'arduino.toolbar.dropdown.borderActive', id: 'arduino.toolbar.dropdown.borderActive',
defaults: { defaults: {
dark: 'focusBorder', dark: 'focusBorder',
light: 'focusBorder', light: 'focusBorder',
hc: 'focusBorder', hcDark: 'focusBorder',
hcLight: 'focusBorder',
}, },
description: "Border color of the Board Selector when it's active", description: "Border color of the Board Selector when it's active",
}, },
{ {
id: 'arduino.toolbar.dropdown.background', id: 'arduino.toolbar.dropdown.background',
defaults: { defaults: {
dark: 'tab.unfocusedActiveBackground', dark: 'tab.unfocusedActiveBackground',
light: 'dropdown.background', light: 'dropdown.background',
hc: 'dropdown.background', hcDark: 'dropdown.background',
hcLight: 'dropdown.background',
}, },
description: 'Background color of the Board Selector.', description: 'Background color of the Board Selector.',
}, },
{ {
id: 'arduino.toolbar.dropdown.label', id: 'arduino.toolbar.dropdown.label',
defaults: { defaults: {
dark: 'dropdown.foreground', dark: 'dropdown.foreground',
light: 'dropdown.foreground', light: 'dropdown.foreground',
hc: 'dropdown.foreground', hcDark: 'dropdown.foreground',
hcLight: 'dropdown.foreground',
}, },
description: 'Font color of the Board Selector.', description: 'Font color of the Board Selector.',
}, },
@ -257,7 +268,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'list.activeSelectionIconForeground', dark: 'list.activeSelectionIconForeground',
light: 'list.activeSelectionIconForeground', light: 'list.activeSelectionIconForeground',
hc: 'list.activeSelectionIconForeground', hcDark: 'list.activeSelectionIconForeground',
hcLight: 'list.activeSelectionIconForeground',
}, },
description: description:
'Color of the selected protocol icon in the Board Selector.', 'Color of the selected protocol icon in the Board Selector.',
@ -267,7 +279,8 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'list.hoverBackground', dark: 'list.hoverBackground',
light: 'list.hoverBackground', light: 'list.hoverBackground',
hc: 'list.hoverBackground', hcDark: 'list.hoverBackground',
hcLight: 'list.hoverBackground',
}, },
description: 'Background color on hover of the Board Selector options.', description: 'Background color on hover of the Board Selector options.',
}, },
@ -276,11 +289,191 @@ export class ArduinoFrontendContribution
defaults: { defaults: {
dark: 'list.activeSelectionBackground', dark: 'list.activeSelectionBackground',
light: 'list.activeSelectionBackground', light: 'list.activeSelectionBackground',
hc: 'list.activeSelectionBackground', hcDark: 'list.activeSelectionBackground',
hcLight: 'list.activeSelectionBackground',
}, },
description: description:
'Background color of the selected board in the Board Selector.', 'Background color of the selected board in the Board Selector.',
} }
); );
} }
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
const warningForeground = theme.getColor('warningForeground');
const warningBackground = theme.getColor('warningBackground');
const focusBorder = theme.getColor('focusBorder');
const contrastBorder = theme.getColor('contrastBorder');
const notificationsBackground = theme.getColor('notifications.background');
const buttonBorder = theme.getColor('button.border');
const buttonBackground = theme.getColor('button.background') || 'none';
const dropdownBackground = theme.getColor('dropdown.background');
const arduinoToolbarButtonBackground = theme.getColor(
'arduino.toolbar.button.background'
);
if (isHighContrast(theme.type)) {
// toolbar items
collector.addRule(`
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-monitor,
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-plotter {
background: transparent;
}
`);
if (contrastBorder) {
collector.addRule(`
.quick-input-widget {
outline: 1px solid ${contrastBorder};
outline-offset: -1px;
}
`);
}
if (focusBorder) {
// customized react-select widget
collector.addRule(`
.arduino-select__option--is-selected {
outline: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.arduino-select__option--is-focused {
outline: 1px dashed ${focusBorder};
}
`);
// boards selector dropdown
collector.addRule(`
#select-board-dialog .selectBoardContainer .list .item:hover {
outline: 1px dashed ${focusBorder};
}
`);
// button hover
collector.addRule(`
.theia-button:hover,
button.theia-button:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
.theia-button {
border: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.component-list-item .header .installed-version:hover:before {
background-color: transparent;
outline: 1px dashed ${focusBorder};
}
`);
// tree node
collector.addRule(`
.theia-TreeNode:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
.quick-input-list .monaco-list-row.focused,
.theia-Tree .theia-TreeNode.theia-mod-selected {
outline: 1px dotted ${focusBorder};
}
`);
collector.addRule(`
div#select-board-dialog .selectBoardContainer .list .item.selected,
.theia-Tree:focus .theia-TreeNode.theia-mod-selected,
.theia-Tree .ReactVirtualized__List:focus .theia-TreeNode.theia-mod-selected {
outline: 1px solid ${focusBorder};
}
`);
// quick input
collector.addRule(`
.quick-input-list .monaco-list-row:hover {
outline: 1px dashed ${focusBorder};
}
`);
// editor tab-bar
collector.addRule(`
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
#theia-main-content-panel .p-TabBar .p-TabBar-tab:hover {
outline: 1px dashed ${focusBorder};
outline-offset: -4px;
}
`);
collector.addRule(`
#theia-main-content-panel .p-TabBar .p-TabBar-tab.p-mod-current {
outline: 1px solid ${focusBorder};
outline-offset: -4px;
}
`);
// boards selector dropdown
collector.addRule(`
.arduino-boards-dropdown-item:hover {
outline: 1px dashed ${focusBorder};
outline-offset: -2px;
}
`);
if (notificationsBackground) {
// notification
collector.addRule(`
.theia-notification-list-item:hover:not(:focus) {
background-color: ${notificationsBackground};
outline: 1px dashed ${focusBorder};
outline-offset: -2px;
}
`);
}
if (arduinoToolbarButtonBackground) {
// toolbar item
collector.addRule(`
.item.arduino-tool-item.toggled .arduino-upload-sketch--toolbar,
.item.arduino-tool-item.toggled .arduino-verify-sketch--toolbar {
background-color: ${arduinoToolbarButtonBackground} !important;
outline: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div {
background: ${arduinoToolbarButtonBackground};
outline: 1px dashed ${focusBorder};
}
`);
}
}
if (dropdownBackground) {
// boards selector dropdown
collector.addRule(`
.arduino-boards-dropdown-item:hover {
background: ${dropdownBackground};
}
`);
}
if (warningForeground && warningBackground) {
// <input> widget with inverted foreground and background colors
collector.addRule(`
.theia-input.warning:focus,
.theia-input.warning::placeholder,
.theia-input.warning {
color: ${warningBackground};
background-color: ${warningForeground};
}
`);
}
if (buttonBorder) {
collector.addRule(`
button.theia-button,
button.theia-button.secondary,
.component-list-item .theia-button.secondary.no-border,
.component-list-item .theia-button.secondary.no-border:hover {
border: 1px solid ${buttonBorder};
}
`);
collector.addRule(`
.component-list-item .header .installed-version:before {
color: ${buttonBackground};
border: 1px solid ${buttonBorder};
}
`);
}
}
}
} }

View File

@ -1,5 +1,5 @@
import '../../src/browser/style/index.css'; import '../../src/browser/style/index.css';
import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command'; import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@ -295,7 +295,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors'; import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager'; import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './contributions/startup-task'; import { StartupTasksExecutor } from './contributions/startup-tasks-executor';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress'; import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon'; import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer'; import { FirstStartupInstaller } from './contributions/first-startup-installer';
@ -341,16 +341,6 @@ import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution'; import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution'; import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
import { DebugSessionFactory } from '@theia/debug/lib/browser/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';
import { ConfigServiceClient } from './config/config-service-client'; import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch'; import { ValidateSketch } from './contributions/validate-sketch';
import { RenameCloudSketch } from './contributions/rename-cloud-sketch'; import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
@ -361,15 +351,29 @@ import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/
import { CreateCloudCopy } from './contributions/create-cloud-copy'; import { CreateCloudCopy } from './contributions/create-cloud-copy';
import { FileResourceResolver } from './theia/filesystem/file-resource'; import { FileResourceResolver } from './theia/filesystem/file-resource';
import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource'; import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource';
import { StylingParticipant } from '@theia/core/lib/browser/styling-service';
import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu';
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
// Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487
import('@theia/core/lib/browser/common-frontend-contribution.js').then(
(theiaCommonContribution) => {
theiaCommonContribution['supportCopy'] = true;
theiaCommonContribution['supportCut'] = true;
theiaCommonContribution['supportPaste'] = true;
}
);
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items // Commands, colors, theme adjustments, and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope(); bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution); bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution); bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution); bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution); bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution); bind(ColorContribution).toService(ArduinoFrontendContribution);
bind(StylingParticipant).toService(ArduinoFrontendContribution);
bind(ArduinoToolbarContribution).toSelf().inSingletonScope(); bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution); bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
@ -722,7 +726,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, PlotterFrontendContribution); Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format); Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors); Contribution.configure(bind, CompilerErrors);
Contribution.configure(bind, StartupTasks); Contribution.configure(bind, StartupTasksExecutor);
Contribution.configure(bind, IndexesUpdateProgress); Contribution.configure(bind, IndexesUpdateProgress);
Contribution.configure(bind, Daemon); Contribution.configure(bind, Daemon);
Contribution.configure(bind, FirstStartupInstaller); Contribution.configure(bind, FirstStartupInstaller);
@ -982,9 +986,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// workaround for themes cannot be removed after registration // workaround for themes cannot be removed after registration
// https://github.com/eclipse-theia/theia/issues/11151 // https://github.com/eclipse-theia/theia/issues/11151
bind(CleanupObsoleteThemes).toSelf().inSingletonScope(); bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService( bind(FrontendApplicationContribution).toService(CleanupObsoleteThemes);
CleanupObsoleteThemes
);
bind(ThemesRegistrationSummary).toSelf().inSingletonScope(); bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
bind(MonacoThemeRegistry).toSelf().inSingletonScope(); bind(MonacoThemeRegistry).toSelf().inSingletonScope();
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry); rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
@ -998,37 +1000,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TypeHierarchyContribution).toSelf().inSingletonScope(); bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution); 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(); bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory); 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();
bind(SidebarBottomMenuWidget).toSelf(); bind(SidebarBottomMenuWidget).toSelf();
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget); rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
@ -1043,4 +1016,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// https://github.com/arduino/arduino-ide/issues/437 // https://github.com/arduino/arduino-ide/issues/437
bind(FileResourceResolver).toSelf().inSingletonScope(); bind(FileResourceResolver).toSelf().inSingletonScope();
rebind(TheiaFileResourceResolver).toService(FileResourceResolver); rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
// Full control over the editor context menu to filter undesired menu items contributed by Theia.
// https://github.com/arduino/arduino-ide/issues/1394
// https://github.com/arduino/arduino-ide/pull/2027#pullrequestreview-1414246614
bind(MonacoEditorMenuContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorMenuContribution).toService(
MonacoEditorMenuContribution
);
}); });

View File

@ -9,13 +9,13 @@ import {
CommandContribution, CommandContribution,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
import { import {
AuthOptions,
AuthenticationService, AuthenticationService,
AuthenticationServiceClient, AuthenticationServiceClient,
AuthenticationSession, AuthenticationSession,
authServerPort,
} from '../../common/protocol/authentication-service'; } from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands'; import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server';
import { AuthOptions } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
@injectable() @injectable()
@ -61,7 +61,7 @@ export class AuthenticationClientService
setOptions(): Promise<void> { setOptions(): Promise<void> {
return this.service.setOptions({ return this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`, redirectUri: `http://localhost:${authServerPort}/callback`,
responseType: 'code', responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'], clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'], domain: this.arduinoPreferences['arduino.auth.domain'],

View File

@ -1,4 +1,4 @@
import * as PQueue from 'p-queue'; import PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command'; import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';

View File

@ -1,26 +1,27 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as moment from 'moment';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { import { nls } from '@theia/core/lib/common/nls';
Contribution, import { isOSX, isWindows } from '@theia/core/lib/common/os';
Command, import { inject, injectable } from '@theia/core/shared/inversify';
MenuModelRegistry, import moment from 'moment';
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol'; import { ConfigService } from '../../common/protocol';
import { nls } from '@theia/core/lib/common'; import { AppService } from '../app-service';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Command,
CommandRegistry,
Contribution,
MenuModelRegistry,
} from './contribution';
@injectable() @injectable()
export class About extends Contribution { export class About extends Contribution {
@inject(ClipboardService) @inject(ClipboardService)
protected readonly clipboardService: ClipboardService; private readonly clipboardService: ClipboardService;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; private readonly configService: ConfigService;
@inject(AppService)
private readonly appService: AppService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, { registry.registerCommand(About.Commands.ABOUT_APP, {
@ -40,17 +41,20 @@ export class About extends Contribution {
}); });
} }
async showAbout(): Promise<void> { private async showAbout(): Promise<void> {
const version = await this.configService.getVersion(); const [appVersion, cliVersion] = await Promise.all([
this.appService.version(),
this.configService.getVersion(),
]);
const buildDate = this.buildDate; const buildDate = this.buildDate;
const detail = (showAll: boolean) => const detail = (showAll: boolean) =>
nls.localize( nls.localize(
'arduino/about/detail', 'arduino/about/detail',
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}', 'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
remote.app.getVersion(), appVersion,
buildDate ? buildDate : nls.localize('', 'dev build'), buildDate ? buildDate : nls.localize('', 'dev build'),
buildDate && showAll ? ` (${this.ago(buildDate)})` : '', buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
version, cliVersion,
nls.localize( nls.localize(
'arduino/about/copyright', 'arduino/about/copyright',
'Copyright © {0} Arduino SA', 'Copyright © {0} Arduino SA',
@ -60,9 +64,7 @@ export class About extends Contribution {
const ok = nls.localize('vscode/issueMainService/ok', 'OK'); const ok = nls.localize('vscode/issueMainService/ok', 'OK');
const copy = nls.localize('vscode/textInputActions/copy', 'Copy'); const copy = nls.localize('vscode/textInputActions/copy', 'Copy');
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy]; const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await remote.dialog.showMessageBox( const { response } = await this.dialogService.showMessageBox({
remote.getCurrentWindow(),
{
message: `${this.applicationName}`, message: `${this.applicationName}`,
title: `${this.applicationName}`, title: `${this.applicationName}`,
type: 'info', type: 'info',
@ -71,23 +73,22 @@ export class About extends Contribution {
noLink: true, noLink: true,
defaultId: buttons.indexOf(ok), defaultId: buttons.indexOf(ok),
cancelId: buttons.indexOf(ok), cancelId: buttons.indexOf(ok),
} });
);
if (buttons[response] === copy) { if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false).trim()); await this.clipboardService.writeText(detail(false).trim());
} }
} }
protected get applicationName(): string { private get applicationName(): string {
return FrontendApplicationConfigProvider.get().applicationName; return FrontendApplicationConfigProvider.get().applicationName;
} }
protected get buildDate(): string | undefined { private get buildDate(): string | undefined {
return FrontendApplicationConfigProvider.get().buildDate; return FrontendApplicationConfigProvider.get().buildDate;
} }
protected ago(isoTime: string): string { private ago(isoTime: string): string {
const now = moment(Date.now()); const now = moment(Date.now());
const other = moment(isoTime); const other = moment(isoTime);
let result = now.diff(other, 'minute'); let result = now.diff(other, 'minute');

View File

@ -1,22 +1,21 @@
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote'; import { FileDialogService } from '@theia/filesystem/lib/browser';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { import {
SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI,
Sketch, Sketch,
SketchContribution,
URI,
} from './contribution'; } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
@injectable() @injectable()
export class AddFile extends SketchContribution { export class AddFile extends SketchContribution {
@inject(FileDialogService) @inject(FileDialogService)
private readonly fileDialogService: FileDialogService; private readonly fileDialogService: FileDialogService; // TODO: use dialogService
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, { registry.registerCommand(AddFile.Commands.ADD_FILE, {
@ -50,7 +49,7 @@ export class AddFile extends SketchContribution {
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri); const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
const exists = await this.fileService.exists(targetUri); const exists = await this.fileService.exists(targetUri);
if (exists) { if (exists) {
const { response } = await remote.dialog.showMessageBox({ const { response } = await this.dialogService.showMessageBox({
type: 'question', type: 'question',
title: nls.localize('arduino/contributions/replaceTitle', 'Replace'), title: nls.localize('arduino/contributions/replaceTitle', 'Replace'),
buttons: [ buttons: [

View File

@ -1,5 +1,4 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
@ -42,9 +41,7 @@ export class AddZipLibrary extends SketchContribution {
private async addZipLibrary(): Promise<void> { private async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri(); const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri)); const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog( const { canceled, filePaths } = await this.dialogService.showOpenDialog({
remote.getCurrentWindow(),
{
title: nls.localize( title: nls.localize(
'arduino/selectZip', 'arduino/selectZip',
"Select a zip file containing the library you'd like to add" "Select a zip file containing the library you'd like to add"
@ -57,8 +54,7 @@ export class AddZipLibrary extends SketchContribution {
extensions: ['zip'], extensions: ['zip'],
}, },
], ],
} });
);
if (!canceled && filePaths.length) { if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]); const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try { try {

View File

@ -1,6 +1,5 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote'; import dateFormat from 'dateformat';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
@ -39,16 +38,13 @@ export class ArchiveSketch extends SketchContribution {
const defaultContainerUri = await this.defaultUri(); const defaultContainerUri = await this.defaultUri();
const defaultUri = defaultContainerUri.resolve(archiveBasename); const defaultUri = defaultContainerUri.resolve(archiveBasename);
const defaultPath = await this.fileService.fsPath(defaultUri); const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog( const { filePath, canceled } = await this.dialogService.showSaveDialog({
remote.getCurrentWindow(),
{
title: nls.localize( title: nls.localize(
'arduino/sketch/saveSketchAs', 'arduino/sketch/saveSketchAs',
'Save sketch folder as...' 'Save sketch folder as...'
), ),
defaultPath, defaultPath,
} });
);
if (!filePath || canceled) { if (!filePath || canceled) {
return; return;
} }

View File

@ -1,5 +1,4 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { import {
DisposableCollection, DisposableCollection,
@ -65,7 +64,7 @@ VID: ${VID}
PID: ${PID} PID: ${PID}
SN: ${SN} SN: ${SN}
`.trim(); `.trim();
await remote.dialog.showMessageBox(remote.getCurrentWindow(), { await this.dialogService.showMessageBox({
message: nls.localize('arduino/board/boardInfo', 'Board Info'), message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'), title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info', type: 'info',

View File

@ -1,26 +1,26 @@
import { injectable } from '@theia/core/shared/inversify'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import type { MaybePromise } from '@theia/core/lib/common/types';
import type { import type {
FrontendApplication, FrontendApplication,
OnWillStopAction, OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application'; } from '@theia/core/lib/browser/frontend-application';
import { nls } from '@theia/core/lib/common/nls';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybePromise } from '@theia/core/lib/common/types';
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import { import {
SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
MenuModelRegistry,
Sketch, Sketch,
SketchContribution,
URI, URI,
} from './contribution'; } from './contribution';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch'; import { SaveAsSketch } from './save-as-sketch';
/** /**
@ -28,6 +28,9 @@ import { SaveAsSketch } from './save-as-sketch';
*/ */
@injectable() @injectable()
export class Close extends SketchContribution { export class Close extends SketchContribution {
@inject(WindowServiceExt)
private readonly windowServiceExt: WindowServiceExt;
private shell: ApplicationShell | undefined; private shell: ApplicationShell | undefined;
override onStart(app: FrontendApplication): MaybePromise<void> { override onStart(app: FrontendApplication): MaybePromise<void> {
@ -56,7 +59,7 @@ export class Close extends SketchContribution {
} }
} }
} }
return remote.getCurrentWindow().close(); return this.windowServiceExt.close();
}, },
}); });
} }
@ -150,9 +153,7 @@ export class Close extends SketchContribution {
} }
private async prompt(isTemp: boolean): Promise<Prompt> { private async prompt(isTemp: boolean): Promise<Prompt> {
const { response } = await remote.dialog.showMessageBox( const { response } = await this.dialogService.showMessageBox({
remote.getCurrentWindow(),
{
message: nls.localize( message: nls.localize(
'arduino/sketch/saveSketch', 'arduino/sketch/saveSketch',
'Save your sketch to open it again later.' 'Save your sketch to open it again later.'
@ -168,8 +169,7 @@ export class Close extends SketchContribution {
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'), nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
], ],
defaultId: 2, // `Save`/`Save As...` button index is the default. defaultId: 2, // `Save`/`Save As...` button index is the default.
} });
);
switch (response) { switch (response) {
case 0: case 0:
return Prompt.DoNotSave; return Prompt.DoNotSave;

View File

@ -67,6 +67,7 @@ import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client'; import { ConfigServiceClient } from '../config/config-service-client';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { DialogService } from '../dialog-service';
export { export {
Command, Command,
@ -115,6 +116,9 @@ export abstract class Contribution
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly menuManager: MainMenuManager; protected readonly menuManager: MainMenuManager;
@inject(DialogService)
protected readonly dialogService: DialogService;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady()); this.appStateService.reachedState('ready').then(() => this.onReady());

View File

@ -1,5 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { Dialog } from '@theia/core/lib/browser/dialogs'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types'; import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
@ -10,11 +8,11 @@ import URI from '@theia/core/lib/common/uri';
import type { Widget } from '@theia/core/shared/@phosphor/widgets'; import type { Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol'; import { SketchesError } from '../../common/protocol';
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
import { Sketch } from '../contributions/contribution'; import { Sketch } from '../contributions/contribution';
import { isNotFound } from '../create/typings'; import { isNotFound } from '../create/typings';
import { Command, CommandRegistry } from './contribution'; import { Command, CommandRegistry } from './contribution';
import { CloudSketchContribution } from './cloud-contribution'; import { CloudSketchContribution } from './cloud-contribution';
import { AppService } from '../app-service';
export interface DeleteSketchParams { export interface DeleteSketchParams {
/** /**
@ -38,6 +36,8 @@ export class DeleteSketch extends CloudSketchContribution {
private readonly shell: ApplicationShell; private readonly shell: ApplicationShell;
@inject(WindowService) @inject(WindowService)
private readonly windowService: WindowService; private readonly windowService: WindowService;
@inject(AppService)
private readonly appService: AppService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, { registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
@ -66,7 +66,7 @@ export class DeleteSketch extends CloudSketchContribution {
} }
const cloudUri = this.createFeatures.cloudUri(sketch); const cloudUri = this.createFeatures.cloudUri(sketch);
if (willNavigateAway !== 'force') { if (willNavigateAway !== 'force') {
const { response } = await remote.dialog.showMessageBox({ const { response } = await this.dialogService.showMessageBox({
title: nls.localizeByDefault('Delete'), title: nls.localizeByDefault('Delete'),
type: 'question', type: 'question',
buttons: [Dialog.CANCEL, Dialog.OK], buttons: [Dialog.CANCEL, Dialog.OK],
@ -120,7 +120,7 @@ export class DeleteSketch extends CloudSketchContribution {
} }
private scheduleDeletion(sketch: Sketch): void { private scheduleDeletion(sketch: Sketch): void {
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch); this.appService.scheduleDeletion(sketch);
} }
private async loadSketch(uri: string): Promise<Sketch | undefined> { private async loadSketch(uri: string): Promise<Sketch | undefined> {

View File

@ -1,11 +1,7 @@
import * as PQueue from 'p-queue'; import PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler, CommandService } from '@theia/core/lib/common/command'; import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
import { import { MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
MenuPath,
CompositeMenuNode,
SubMenuOptions,
} from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
@ -143,19 +139,6 @@ export abstract class Examples extends SketchContribution {
}): void; }): void;
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
const groupPath =
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
}
// Registering the same submenu multiple times has no side-effect. // Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300 // TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu( registry.registerSubmenu(

View File

@ -1,4 +1,4 @@
import * as PQueue from 'p-queue'; import PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';

View File

@ -1,4 +1,7 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { import {
@ -120,6 +123,7 @@ export class InoLanguage extends SketchContribution {
return; return;
} }
const release = await this.languageServerStartMutex.acquire(); const release = await this.languageServerStartMutex.acquire();
const toDisposeOnRelease = new DisposableCollection();
try { try {
await this.hostedPluginEvents.didStart; await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn }); const details = await this.boardsService.getBoardDetails({ fqbn });
@ -179,12 +183,13 @@ export class InoLanguage extends SketchContribution {
]); ]);
this.languageServerFqbn = await Promise.race([ this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) => new Promise<undefined>((_, reject) => {
setTimeout( const timer = setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)), () => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000 20_000
) );
), toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
}),
this.commandService.executeCommand<string>( this.commandService.executeCommand<string>(
'arduino.languageserver.start', 'arduino.languageserver.start',
{ {
@ -206,6 +211,7 @@ export class InoLanguage extends SketchContribution {
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e); console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
this.languageServerFqbn = undefined; this.languageServerFqbn = undefined;
} finally { } finally {
toDisposeOnRelease.dispose();
release(); release();
} }
} }

View File

@ -8,7 +8,7 @@ import {
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common'; import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings'; import { Settings } from '../dialogs/settings/settings';
import debounce = require('lodash.debounce'); import debounce from 'lodash.debounce';
@injectable() @injectable()
export class InterfaceScale extends Contribution { export class InterfaceScale extends Contribution {

View File

@ -1,5 +1,4 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
@ -9,7 +8,7 @@ import {
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls';
@injectable() @injectable()
export class OpenSketchExternal extends SketchContribution { export class OpenSketchExternal extends SketchContribution {
@ -41,7 +40,7 @@ export class OpenSketchExternal extends SketchContribution {
if (exists) { if (exists) {
const fsPath = await this.fileService.fsPath(new URI(uri)); const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) { if (fsPath) {
remote.shell.showItemInFolder(fsPath); window.electronTheiaCore.showItemInFolder(fsPath);
} }
} }
} }

View File

@ -118,6 +118,7 @@ export class OpenSketchFiles extends SketchContribution {
fileService: this.fileService, fileService: this.fileService,
sketchesService: this.sketchesService, sketchesService: this.sketchesService,
labelProvider: this.labelProvider, labelProvider: this.labelProvider,
dialogService: this.dialogService,
}); });
if (movedSketch) { if (movedSketch) {
this.workspaceService.open(new URI(movedSketch.uri), { this.workspaceService.open(new URI(movedSketch.uri), {

View File

@ -1,4 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service';
@ -18,6 +17,7 @@ import {
SketchContribution, SketchContribution,
URI, URI,
} from './contribution'; } from './contribution';
import { DialogService } from '../dialog-service';
export type SketchLocation = string | URI | SketchRef; export type SketchLocation = string | URI | SketchRef;
export namespace SketchLocation { export namespace SketchLocation {
@ -83,9 +83,7 @@ export class OpenSketch extends SketchContribution {
private async selectSketch(): Promise<Sketch | undefined> { private async selectSketch(): Promise<Sketch | undefined> {
const defaultPath = await this.defaultPath(); const defaultPath = await this.defaultPath();
const { filePaths } = await remote.dialog.showOpenDialog( const { filePaths } = await this.dialogService.showOpenDialog({
remote.getCurrentWindow(),
{
defaultPath, defaultPath,
properties: ['createDirectory', 'openFile'], properties: ['createDirectory', 'openFile'],
filters: [ filters: [
@ -94,8 +92,7 @@ export class OpenSketch extends SketchContribution {
extensions: ['ino', 'pde'], extensions: ['ino', 'pde'],
}, },
], ],
} });
);
if (!filePaths.length) { if (!filePaths.length) {
return undefined; return undefined;
} }
@ -115,6 +112,7 @@ export class OpenSketch extends SketchContribution {
fileService: this.fileService, fileService: this.fileService,
sketchesService: this.sketchesService, sketchesService: this.sketchesService,
labelProvider: this.labelProvider, labelProvider: this.labelProvider,
dialogService: this.dialogService,
}); });
} }
} }
@ -134,14 +132,16 @@ export async function promptMoveSketch(
fileService: FileService; fileService: FileService;
sketchesService: SketchesService; sketchesService: SketchesService;
labelProvider: LabelProvider; labelProvider: LabelProvider;
dialogService: DialogService;
} }
): Promise<Sketch | undefined> { ): Promise<Sketch | undefined> {
const { fileService, sketchesService, labelProvider } = options; const { fileService, sketchesService, labelProvider, dialogService } =
options;
const uri = const uri =
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri); sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
const name = uri.path.name; const name = uri.path.name;
const nameWithExt = labelProvider.getName(uri); const nameWithExt = labelProvider.getName(uri);
const { response } = await remote.dialog.showMessageBox({ const { response } = await dialogService.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'), title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question', type: 'question',
buttons: [ buttons: [
@ -160,7 +160,7 @@ export async function promptMoveSketch(
const newSketchUri = uri.parent.resolve(name); const newSketchUri = uri.parent.resolve(name);
const exists = await fileService.exists(newSketchUri); const exists = await fileService.exists(newSketchUri);
if (exists) { if (exists) {
await remote.dialog.showMessageBox({ await dialogService.showMessageBox({
type: 'error', type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'), title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize( message: nls.localize(

View File

@ -1,5 +1,4 @@
import { injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { import {
Contribution, Contribution,
@ -9,14 +8,18 @@ import {
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls';
import { AppService } from '../app-service';
@injectable() @injectable()
export class QuitApp extends Contribution { export class QuitApp extends Contribution {
@inject(AppService)
private readonly appService: AppService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
if (!isOSX) { if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, { registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(), execute: () => this.appService.quit(),
}); });
} }
} }

View File

@ -1,4 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable'; import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
import { Saveable } from '@theia/core/lib/browser/saveable'; import { Saveable } from '@theia/core/lib/browser/saveable';
@ -8,7 +7,7 @@ import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTask } from '../../electron-common/startup-task'; import { StartupTasks } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution'; import { CloudSketchContribution } from './cloud-contribution';
@ -95,7 +94,7 @@ export class SaveAsSketch extends CloudSketchContribution {
if (markAsRecentlyOpened) { if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri); this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
} }
const options: WorkspaceInput & StartupTask.Owner = { const options: WorkspaceInput & StartupTasks = {
preserveWindow: true, preserveWindow: true,
tasks: [], tasks: [],
}; };
@ -165,16 +164,13 @@ export class SaveAsSketch extends CloudSketchContribution {
): Promise<string | undefined> { ): Promise<string | undefined> {
let sketchFolderDestinationUri: string | undefined; let sketchFolderDestinationUri: string | undefined;
while (!sketchFolderDestinationUri) { while (!sketchFolderDestinationUri) {
const { filePath } = await remote.dialog.showSaveDialog( const { filePath } = await this.dialogService.showSaveDialog({
remote.getCurrentWindow(),
{
title: nls.localize( title: nls.localize(
'arduino/sketch/saveFolderAs', 'arduino/sketch/saveFolderAs',
'Save sketch folder as...' 'Save sketch folder as...'
), ),
defaultPath, defaultPath,
} });
);
if (!filePath) { if (!filePath) {
return undefined; return undefined;
} }
@ -225,13 +221,10 @@ ${dialogContent.details}
${dialogContent.question}`.trim(); ${dialogContent.question}`.trim();
defaultPath = filePath; defaultPath = filePath;
const { response } = await remote.dialog.showMessageBox( const { response } = await this.dialogService.showMessageBox({
remote.getCurrentWindow(),
{
message, message,
buttons: [Dialog.CANCEL, Dialog.YES], buttons: [Dialog.CANCEL, Dialog.YES],
} });
);
// cancel // cancel
if (response === 0) { if (response === 0) {
return undefined; return undefined;

View File

@ -1,52 +0,0 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { injectable } from '@theia/core/shared/inversify';
import { StartupTask } from '../../electron-common/startup-task';
import { Contribution } from './contribution';
@injectable()
export class StartupTasks extends Contribution {
override onReady(): void {
ipcRenderer.once(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
(_: IpcRendererEvent, args: unknown) => {
console.debug(
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
args
)}`
);
if (!StartupTask.has(args)) {
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
return;
}
const tasks = args.tasks;
if (tasks.length) {
console.log(`Executing startup tasks:`);
tasks.forEach(({ command, args = [] }) => {
console.log(
` - '${command}' ${
args.length ? `, args: ${JSON.stringify(args)}` : ''
}`
);
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
);
});
}
}
);
const { id } = remote.getCurrentWindow();
console.debug(
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
);
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
}
}

View File

@ -0,0 +1,65 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import {
hasStartupTasks,
StartupTasks,
} from '../../electron-common/startup-task';
import { AppService } from '../app-service';
import { Contribution } from './contribution';
@injectable()
export class StartupTasksExecutor extends Contribution {
@inject(AppService)
private readonly appService: AppService;
private readonly toDispose = new DisposableCollection();
@postConstruct()
protected override init(): void {
super.init();
this.toDispose.push(
this.appService.registerStartupTasksHandler((tasks) =>
this.handleStartupTasks(tasks)
)
);
}
onStop(): void {
this.toDispose.dispose();
}
private async handleStartupTasks(tasks: StartupTasks): Promise<void> {
console.debug(
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
tasks
)}`
);
if (!hasStartupTasks(tasks)) {
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
return;
}
await this.appStateService.reachedState('ready');
console.log(`Executing startup tasks:`);
tasks.tasks.forEach(({ command, args = [] }) => {
console.log(
` - '${command}' ${
args.length ? `, args: ${JSON.stringify(args)}` : ''
}`
);
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
);
});
}
}

View File

@ -1,4 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util'; import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
@ -180,15 +179,12 @@ export class ValidateSketch extends CloudSketchContribution {
message: string, message: string,
buttons: string[] = [Dialog.CANCEL, Dialog.OK] buttons: string[] = [Dialog.CANCEL, Dialog.OK]
): Promise<boolean> { ): Promise<boolean> {
const { response } = await remote.dialog.showMessageBox( const { response } = await this.dialogService.showMessageBox({
remote.getCurrentWindow(),
{
title, title,
message, message,
type: 'warning', type: 'warning',
buttons, buttons,
} });
);
// cancel // cancel
if (response === 0) { if (response === 0) {
return false; return false;

View File

@ -4,7 +4,7 @@ import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { Sketch } from '../../common/protocol'; import { Sketch } from '../../common/protocol';
import { AuthenticationSession } from '../../node/auth/types'; import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider'; import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';

View File

@ -0,0 +1,15 @@
import type {
MessageBoxOptions,
MessageBoxReturnValue,
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue,
} from '../electron-common/electron-arduino';
export const DialogService = Symbol('DialogService');
export interface DialogService {
showMessageBox(options: MessageBoxOptions): Promise<MessageBoxReturnValue>;
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnValue>;
showSaveDialog(options: SaveDialogOptions): Promise<SaveDialogReturnValue>;
}

View File

@ -1,12 +1,14 @@
import * as React from '@theia/core/shared/react'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { inject, injectable } from '@theia/core/shared/inversify'; import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets'; import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { nls } from '@theia/core/lib/common/nls';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Message } from '@theia/core/shared/@phosphor/messaging'; import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from '@theia/core/electron-shared/@electron/remote'; import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser'; import * as React from '@theia/core/shared/react';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { CreateApi } from '../create/create-api'; import { CreateApi } from '../create/create-api';
import { nls } from '@theia/core/lib/common'; import { AbstractDialog } from '../theia/dialogs/dialogs';
const RadioButton = (props: { const RadioButton = (props: {
id: string; id: string;
@ -35,15 +37,18 @@ export const ShareSketchComponent = ({
treeNode, treeNode,
createApi, createApi,
domain = 'https://create.arduino.cc', domain = 'https://create.arduino.cc',
writeClipboard,
}: { }: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
treeNode: any; treeNode: any;
createApi: CreateApi; createApi: CreateApi;
domain?: string; domain?: string;
writeClipboard: (text: string) => MaybePromise<void>;
}): React.ReactElement => { }): React.ReactElement => {
const [loading, setloading] = React.useState<boolean>(false); const [loading, setLoading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => { const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setloading(true); setLoading(true);
const sketch = await createApi.editSketch({ const sketch = await createApi.editSketch({
id: treeNode.sketchId, id: treeNode.sketchId,
params: { params: {
@ -52,7 +57,7 @@ export const ShareSketchComponent = ({
}); });
// setPublicVisibility(sketch.is_public); // setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public; treeNode.isPublic = sketch.is_public;
setloading(false); setLoading(false);
}; };
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`; const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
@ -100,7 +105,7 @@ export const ShareSketchComponent = ({
className="theia-input" className="theia-input"
/> />
<button <button
onClick={() => clipboard.writeText(sketchLink)} onClick={() => writeClipboard(sketchLink)}
value="copy" value="copy"
className="theia-button secondary" className="theia-button secondary"
> >
@ -121,44 +126,52 @@ export const ShareSketchComponent = ({
); );
}; };
@injectable()
export class ShareSketchWidget extends ReactWidget { export class ShareSketchWidget extends ReactWidget {
constructor(private treeNode: any, private createApi: CreateApi) { private readonly writeClipboard = (text: string) =>
this.clipboardService.writeText(text);
constructor(
private treeNode: TreeNode,
private createApi: CreateApi,
private clipboardService: ClipboardService
) {
super(); super();
} }
protected render(): React.ReactNode { protected override render(): React.ReactNode {
return ( return (
<ShareSketchComponent <ShareSketchComponent
treeNode={this.treeNode} treeNode={this.treeNode}
createApi={this.createApi} createApi={this.createApi}
writeClipboard={this.writeClipboard}
/> />
); );
} }
} }
@injectable()
export class ShareSketchDialogProps extends DialogProps { export class ShareSketchDialogProps extends DialogProps {
readonly node: any; readonly node: TreeNode;
readonly createApi: CreateApi; readonly createApi: CreateApi;
readonly clipboardService: ClipboardService;
} }
@injectable()
export class ShareSketchDialog extends AbstractDialog<void> { export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget; protected widget: ShareSketchWidget;
constructor( constructor(protected override readonly props: ShareSketchDialogProps) {
@inject(ShareSketchDialogProps)
protected override readonly props: ShareSketchDialogProps
) {
super({ title: props.title }); super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog'); this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(props.node, props.createApi); this.widget = new ShareSketchWidget(
props.node,
props.createApi,
props.clipboardService
);
} }
get value(): void { override get value(): void {
return; return;
} }
protected override onAfterAttach(msg: Message): void { protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) { if (this.widget.isAttached) {
Widget.detach(this.widget); Widget.detach(this.widget);

View File

@ -157,7 +157,7 @@ export const FirmwareUploaderComponent = ({
options={firmwareOptions} options={firmwareOptions}
value={selectedFirmware} value={selectedFirmware}
tabSelectsValue={false} tabSelectsValue={false}
onChange={(value) => { onChange={(value: FirmwareOption | null) => {
if (value) { if (value) {
setInstallFeedback(null); setInstallFeedback(null);
setSelectedFirmware(value); setSelectedFirmware(value);

View File

@ -1,10 +1,15 @@
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import ReactMarkdown from 'react-markdown'; // @ts-expect-error see https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1319854183
import type { Options } from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater'; import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar'; import ProgressBar from '../../components/ProgressBar';
const ReactMarkdown = React.lazy<React.ComponentType<Options>>(
// @ts-expect-error see above
() => import('react-markdown')
);
export interface UpdateProgress { export interface UpdateProgress {
progressInfo?: ProgressInfo | undefined; progressInfo?: ProgressInfo | undefined;
downloadFinished?: boolean; downloadFinished?: boolean;
@ -15,6 +20,7 @@ export interface UpdateProgress {
export interface IDEUpdaterComponentProps { export interface IDEUpdaterComponentProps {
updateInfo: UpdateInfo; updateInfo: UpdateInfo;
updateProgress: UpdateProgress; updateProgress: UpdateProgress;
openExternal: (url: string) => undefined;
} }
export const IDEUpdaterComponent = ({ export const IDEUpdaterComponent = ({
@ -25,6 +31,7 @@ export const IDEUpdaterComponent = ({
progressInfo, progressInfo,
error, error,
}, },
openExternal,
}: IDEUpdaterComponentProps): React.ReactElement => { }: IDEUpdaterComponentProps): React.ReactElement => {
const { version, releaseNotes } = updateInfo; const { version, releaseNotes } = updateInfo;
const [changelog, setChangelog] = React.useState<string>(''); const [changelog, setChangelog] = React.useState<string>('');
@ -95,13 +102,18 @@ export const IDEUpdaterComponent = ({
{changelog && ( {changelog && (
<div className="dialogRow changelog-container"> <div className="dialogRow changelog-container">
<div className="changelog"> <div className="changelog">
<React.Suspense
fallback={
<div className="fallback">
<div className="spinner" />
</div>
}
>
<ReactMarkdown <ReactMarkdown
components={{ components={{
// @ts-expect-error see imports. There is no ESM type-only import in CommonJS modules.
a: ({ href, children, ...props }) => ( a: ({ href, children, ...props }) => (
<a <a onClick={() => href && openExternal(href)} {...props}>
onClick={() => href && shell.openExternal(href)}
{...props}
>
{children} {children}
</a> </a>
), ),
@ -109,6 +121,7 @@ export const IDEUpdaterComponent = ({
> >
{changelog} {changelog}
</ReactMarkdown> </ReactMarkdown>
</React.Suspense>
</div> </div>
</div> </div>
)} )}

View File

@ -18,8 +18,6 @@ import {
import { LocalStorageService } from '@theia/core/lib/browser'; import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
@injectable() @injectable()
export class IDEUpdaterDialogProps extends DialogProps {} export class IDEUpdaterDialogProps extends DialogProps {}
@ -76,11 +74,15 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
<IDEUpdaterComponent <IDEUpdaterComponent
updateInfo={this.updateInfo} updateInfo={this.updateInfo}
updateProgress={this.updateProgress} updateProgress={this.updateProgress}
openExternal={this.openExternal}
/> />
) )
); );
} }
private readonly openExternal = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
get value(): UpdateInfo | undefined { get value(): UpdateInfo | undefined {
return this.updateInfo; return this.updateInfo;
} }
@ -164,7 +166,7 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
} }
private openDownloadPage(): void { private openDownloadPage(): void {
this.windowService.openNewWindow(DOWNLOAD_PAGE_URL, { external: true }); this.openExternal('https://www.arduino.cc/en/software');
this.close(); this.close();
} }

View File

@ -14,11 +14,9 @@ import {
Monitor, Monitor,
MonitorManagerProxyClient, MonitorManagerProxyClient,
MonitorManagerProxyFactory, MonitorManagerProxyFactory,
} from '../common/protocol/monitor-service';
import {
PluggableMonitorSettings,
MonitorSettings, MonitorSettings,
} from '../node/monitor-settings/monitor-settings-provider'; PluggableMonitorSettings,
} from '../common/protocol/monitor-service';
import { BoardsConfig } from './boards/boards-config'; import { BoardsConfig } from './boards/boards-config';
import { BoardsServiceProvider } from './boards/boards-service-provider'; import { BoardsServiceProvider } from './boards/boards-service-provider';

View File

@ -10,10 +10,10 @@ import {
monitorConnectionStatusEquals, monitorConnectionStatusEquals,
MonitorEOL, MonitorEOL,
MonitorManagerProxyClient, MonitorManagerProxyClient,
MonitorSettings,
MonitorState, MonitorState,
} from '../common/protocol'; } from '../common/protocol';
import { isNullOrUndefined } from '../common/utils'; import { isNullOrUndefined } from '../common/utils';
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
@injectable() @injectable()
export class MonitorModel implements FrontendApplicationContribution { export class MonitorModel implements FrontendApplicationContribution {

View File

@ -23,9 +23,9 @@ import { nls } from '@theia/core/lib/common';
import { import {
MonitorEOL, MonitorEOL,
MonitorManagerProxyClient, MonitorManagerProxyClient,
MonitorSettings,
} from '../../../common/protocol'; } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model'; import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable() @injectable()

View File

@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event'; import { Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { areEqual, FixedSizeList as List } from 'react-window'; import { areEqual, FixedSizeList as List } from 'react-window';
import dateFormat = require('dateformat'); import dateFormat from 'dateformat';
import { messagesToLines, truncateLines } from './monitor-utils'; import { messagesToLines, truncateLines } from './monitor-utils';
import { MonitorManagerProxyClient } from '../../../common/protocol'; import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model'; import { MonitorModel } from '../../monitor-model';

View File

@ -1,26 +1,16 @@
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeService } from '@theia/core/lib/browser/theming';
import { injectable, inject } from '@theia/core/shared/inversify'; import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { import { DisposableCollection } from '@theia/core/lib/common/disposable';
Command, import type { MenuModelRegistry } from '@theia/core/lib/common/menu';
CommandRegistry, import { nls } from '@theia/core/lib/common/nls';
MaybePromise, import { inject, injectable } from '@theia/core/shared/inversify';
MenuModelRegistry, import queryString from 'query-string';
} from '@theia/core';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { Contribution } from '../../contributions/contribution';
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
import { ipcRenderer } from '@theia/electron/shared/electron';
import { MonitorManagerProxyClient } from '../../../common/protocol'; import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider'; import { Contribution } from '../../contributions/contribution';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { MonitorModel } from '../../monitor-model'; import { MonitorModel } from '../../monitor-model';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import {
CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW,
} from '../../../common/ipc-communication';
import { nls } from '@theia/core/lib/common/nls';
const queryString = require('query-string');
export namespace SerialPlotterContribution { export namespace SerialPlotterContribution {
export namespace Commands { export namespace Commands {
@ -44,38 +34,31 @@ export namespace SerialPlotterContribution {
@injectable() @injectable()
export class PlotterFrontendContribution extends Contribution { export class PlotterFrontendContribution extends Contribution {
protected window: Window | null; private readonly endpointUrl = new Endpoint({ path: '/plotter' })
protected url: string; .getRestUrl()
protected wsPort: number; .toString();
private readonly toDispose = new DisposableCollection();
private _plotterUrl: string | undefined;
@inject(MonitorModel) @inject(MonitorModel)
protected readonly model: MonitorModel; private readonly model: MonitorModel;
@inject(ThemeService) @inject(ThemeService)
protected readonly themeService: ThemeService; private readonly themeService: ThemeService;
@inject(MonitorManagerProxyClient) @inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient; private readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider) override onStart(): void {
protected readonly boardsServiceProvider: BoardsServiceProvider; this.toDispose.push(
window.electronArduino.registerPlotterWindowCloseHandler(() => {
override onStart(app: FrontendApplication): MaybePromise<void> { this._plotterUrl = undefined;
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString(); })
);
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
if (!!this.window) {
this.window = null;
}
});
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset()); this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
return super.onStart(app);
} }
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, { registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
execute: this.startPlotter.bind(this), execute: () => this.startPlotter(),
}); });
registry.registerCommand(SerialPlotterContribution.Commands.RESET, { registry.registerCommand(SerialPlotterContribution.Commands.RESET, {
execute: () => this.reset(), execute: () => this.reset(),
@ -85,7 +68,7 @@ export class PlotterFrontendContribution extends Contribution {
{ {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'right', ArduinoToolbar.is(widget) && widget.side === 'right',
execute: this.startPlotter.bind(this), execute: () => this.startPlotter(),
} }
); );
} }
@ -98,10 +81,13 @@ export class PlotterFrontendContribution extends Contribution {
}); });
} }
async startPlotter(): Promise<void> { private async startPlotter(forceReload = false): Promise<void> {
await this.monitorManagerProxy.startMonitor(); await this.monitorManagerProxy.startMonitor();
if (!!this.window) { if (this._plotterUrl) {
ipcRenderer.send(SHOW_PLOTTER_WINDOW); window.electronArduino.showPlotterWindow({
url: this._plotterUrl,
forceReload,
});
return; return;
} }
const wsPort = this.monitorManagerProxy.getWebSocketPort(); const wsPort = this.monitorManagerProxy.getWebSocketPort();
@ -117,26 +103,30 @@ export class PlotterFrontendContribution extends Contribution {
} }
} }
protected async open(wsPort: number): Promise<void> { private open(wsPort: number): void {
const initConfig = { const initConfig = {
darkTheme: this.themeService.getCurrentTheme().type === 'dark', darkTheme: this.isDarkTheme,
wsPort, wsPort,
serialPort: this.model.serialPort, serialPort: this.model.serialPort,
}; };
const urlWithParams = queryString.stringifyUrl( this._plotterUrl = queryString.stringifyUrl(
{ {
url: this.url, url: this.endpointUrl,
query: initConfig, query: initConfig,
}, },
{ arrayFormat: 'comma' } { arrayFormat: 'comma' }
); );
this.window = window.open(urlWithParams, 'serialPlotter'); window.electronArduino.showPlotterWindow({ url: this._plotterUrl });
} }
protected async reset(): Promise<void> { private get isDarkTheme(): boolean {
if (!!this.window) { const themeType = this.themeService.getCurrentTheme().type;
this.window.close(); return themeType === 'dark' || themeType === 'hc';
await this.startPlotter(); }
private async reset(): Promise<void> {
if (this._plotterUrl) {
await this.startPlotter(true);
} }
} }
} }

View File

@ -49,14 +49,3 @@
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .arduino-select__option--is-selected {
outline: 1px solid var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .arduino-select__option--is-focused {
outline: 1px dashed var(--theia-focusBorder);
}

View File

@ -276,16 +276,6 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
border-top: 1px solid var(--theia-dropdown-border); border-top: 1px solid var(--theia-dropdown-border);
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc #select-board-dialog .selectBoardContainer .list .item:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc div#select-board-dialog .selectBoardContainer .list .item.selected {
outline: 1px solid var(--theia-focusBorder);
}
@media only screen and (max-height: 400px) { @media only screen and (max-height: 400px) {
div.dialogContent.select-board-dialog > div.head { div.dialogContent.select-board-dialog > div.head {
display: none; display: none;

View File

@ -41,6 +41,17 @@
overflow: auto; overflow: auto;
padding: 0 12px; padding: 0 12px;
cursor: text; cursor: text;
width: 100%;
}
.ide-updater-dialog .changelog .fallback {
min-height: 180px;
width: 100%;
display: flex;
}
.ide-updater-dialog .changelog .fallback .spinner {
align-self: center;
} }
.dialogContent.ide-updater-dialog .dialogContent.ide-updater-dialog
@ -104,3 +115,10 @@
margin-left: 79px; margin-left: 79px;
margin-right: auto; margin-right: auto;
} }
/* https://github.com/arduino/arduino-ide/pull/2027#issuecomment-1533174638 */
/* https://github.com/eclipse-theia/theia/commit/1b5ff9ee459df14cedc0e8266dd02dae93fcd1bf#diff-d8d45a890507a01141c010ad4a6891edf2ae727cfa6dfe604cebbd667812cccbR68 */
/* Use normal whitespace handling for the changelog content in the update dialog. */
.p-Widget.dialogOverlay .dialogContent.ide-updater-dialog {
white-space: normal;
}

View File

@ -54,12 +54,6 @@ body.theia-dark {
background-color: var(--theia-warningBackground); background-color: var(--theia-warningBackground);
} }
.hc-black.hc-theia.theia-hc .theia-input.warning,
.hc-black.hc-theia.theia-hc .theia-input.warning::placeholder {
color: var(--theia-warningBackground);
background-color: var(--theia-warningForeground);
}
.theia-input.error:focus { .theia-input.error:focus {
outline-width: 1px; outline-width: 1px;
outline-style: solid; outline-style: solid;
@ -170,25 +164,6 @@ button.theia-button.message-box-dialog-button {
font-size: 14px; font-size: 14px;
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc button.theia-button:hover,
.hc-black.hc-theia.theia-hc .theia-button:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc button.theia-button,
.hc-black.hc-theia.theia-hc .theia-button,
.hc-black.hc-theia.theia-hc button.theia-button.secondary {
border: 1px solid var(--theia-button-border);
}
.hc-black.hc-theia.theia-hc .theia-notification-list-item:hover:not(:focus) {
background-color: var(--theia-notifications-background);
outline: 1px dashed var(--theia-focusBorder);
outline-offset: -2px;
}
.debug-toolbar .debug-action>div { .debug-toolbar .debug-action>div {
font-family: var(--theia-ui-font-family); font-family: var(--theia-ui-font-family);
font-size: var(--theia-ui-font-size0); font-size: var(--theia-ui-font-size0);

View File

@ -219,15 +219,3 @@ div.filterable-list-container > div > div > div > div:nth-child(1) > div.separat
width: 65px; width: 65px;
min-width: 65px; min-width: 65px;
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:hover:before {
background-color: transparent;
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:before {
color: var(--theia-button-background);
border: 1px solid var(--theia-button-border);
}

View File

@ -137,24 +137,6 @@
color: var(--theia-titleBar-activeForeground); color: var(--theia-titleBar-activeForeground);
} }
.p-TabBar-toolbar .item > div.arduino-toggle-advanced-mode {
display: flex;
width: 24px;
height: 24px;
justify-content: center;
align-items: center;
}
.arduino-toggle-advanced-mode-icon {
mask: none;
-webkit-mask: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-titleBar-activeBackground);
}
.arduino-open-boards-control-icon { .arduino-open-boards-control-icon {
mask: none; mask: none;
-webkit-mask: none; -webkit-mask: none;
@ -202,61 +184,6 @@
background-color: var(--theia-terminal-background); background-color: var(--theia-terminal-background);
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div {
background: var(--theia-arduino-toolbar-button-background);
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-plotter,
.hc-black.hc-theia.theia-hc .p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-monitor {
background: transparent;
}
.hc-black.hc-theia.theia-hc .item.arduino-tool-item.toggled .arduino-verify-sketch--toolbar,
.hc-black.hc-theia.theia-hc .item.arduino-tool-item.toggled .arduino-upload-sketch--toolbar {
background-color: var(--theia-arduino-toolbar-button-background) !important;
outline: 1px solid var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .arduino-boards-dropdown-item:hover {
background: var(--theia-dropdown-background);
}
.hc-black.hc-theia.theia-hc .arduino-boards-dropdown-item:hover {
outline: 1px dashed var(--theia-focusBorder);
outline-offset: -2px;
}
.hc-black.hc-theia.theia-hc #theia-main-content-panel .p-TabBar .p-TabBar-tab.p-mod-current {
outline: 1px solid var(--theia-focusBorder);
outline-offset: -4px;
}
.hc-black.hc-theia.theia-hc #theia-main-content-panel .p-TabBar .p-TabBar-tab:hover {
outline: 1px dashed var(--theia-focusBorder);
outline-offset: -4px;
}
.hc-black.hc-theia.theia-hc .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .quick-input-list .monaco-list-row.focused {
outline: 1px dotted var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .quick-input-list .monaco-list-row:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .quick-input-widget {
outline: 1px solid var(--theia-contrastBorder);
outline-offset: -1px;
}
.monaco-hover .hover-row.markdown-hover:first-child p { .monaco-hover .hover-row.markdown-hover:first-child p {
margin-top: 8px; margin-top: 8px;
} }

View File

@ -80,19 +80,3 @@ widget width.
background: var(--theia-list-inactiveSelectionBackground); background: var(--theia-list-inactiveSelectionBackground);
color: var(--theia-list-inactiveSelectionForeground) !important; color: var(--theia-list-inactiveSelectionForeground) !important;
} }
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .theia-TreeNode:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .theia-Tree .theia-TreeNode.theia-mod-selected {
outline: 1px dotted var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .theia-Tree:focus .theia-TreeNode.theia-mod-selected,
.hc-black.hc-theia.theia-hc .theia-Tree .ReactVirtualized__List:focus .theia-TreeNode.theia-mod-selected {
outline: 1px solid var(--theia-focusBorder);
}

View File

@ -20,8 +20,7 @@ import { ArduinoDaemon } from '../../../common/protocol';
import { assertUnreachable } from '../../../common/utils'; import { assertUnreachable } from '../../../common/utils';
import { CreateFeatures } from '../../create/create-features'; import { CreateFeatures } from '../../create/create-features';
import { NotificationCenter } from '../../notification-center'; import { NotificationCenter } from '../../notification-center';
import debounce = require('lodash.debounce'); import debounce from 'lodash.debounce';
import isOnline = require('is-online');
@injectable() @injectable()
export class IsOnline implements FrontendApplicationContribution { export class IsOnline implements FrontendApplicationContribution {
@ -30,10 +29,11 @@ export class IsOnline implements FrontendApplicationContribution {
private stopped = false; private stopped = false;
onStart(): void { onStart(): void {
import('is-online').then((module) => {
const checkOnline = async () => { const checkOnline = async () => {
if (!this.stopped) { if (!this.stopped) {
try { try {
const online = await isOnline(); const online = await module.default();
this.setOnline(online); this.setOnline(online);
} finally { } finally {
window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval
@ -41,6 +41,7 @@ export class IsOnline implements FrontendApplicationContribution {
} }
}; };
checkOnline(); checkOnline();
});
} }
onStop(): void { onStop(): void {
@ -56,7 +57,7 @@ export class IsOnline implements FrontendApplicationContribution {
return this.onDidChangeOnlineEmitter.event; return this.onDidChangeOnlineEmitter.event;
} }
private setOnline(online: boolean) { private setOnline(online: boolean): void {
const oldOnline = this._online; const oldOnline = this._online;
this._online = online; this._online = online;
if (!this.stopped && this._online !== oldOnline) { if (!this.stopped && this._online !== oldOnline) {

View File

@ -4,7 +4,7 @@ import {
TabBarRenderer as TheiaTabBarRenderer, TabBarRenderer as TheiaTabBarRenderer,
ToolbarAwareTabBar as TheiaToolbarAwareTabBar, ToolbarAwareTabBar as TheiaToolbarAwareTabBar,
} from '@theia/core/lib/browser/shell/tab-bars'; } from '@theia/core/lib/browser/shell/tab-bars';
import debounce = require('lodash.debounce'); import debounce from 'lodash.debounce';
export class TabBarRenderer extends TheiaTabBarRenderer { export class TabBarRenderer extends TheiaTabBarRenderer {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -25,8 +25,8 @@ const builtInThemeIds = new Set(
[ [
ArduinoThemes.light, ArduinoThemes.light,
ArduinoThemes.dark, ArduinoThemes.dark,
BuiltinThemeProvider.hcLightTheme,
BuiltinThemeProvider.hcTheme, BuiltinThemeProvider.hcTheme,
// TODO: add the HC light theme after Theia 1.36
].map(({ id }) => id) ].map(({ id }) => id)
); );
const deprecatedThemeIds = new Set( const deprecatedThemeIds = new Set(
@ -37,7 +37,14 @@ const deprecatedThemeIds = new Set(
export const lightThemeLabel = nls.localize('arduino/theme/light', 'Light'); export const lightThemeLabel = nls.localize('arduino/theme/light', 'Light');
export const darkThemeLabel = nls.localize('arduino/theme/dark', 'Dark'); export const darkThemeLabel = nls.localize('arduino/theme/dark', 'Dark');
export const hcThemeLabel = nls.localize('arduino/theme/hc', 'High Contrast'); export const hcLightThemeLabel = nls.localize(
'arduino/theme/hcLight',
'Light High Contrast'
);
export const hcThemeLabel = nls.localize(
'arduino/theme/hc',
'Dark High Contrast'
);
export function userThemeLabel(theme: Theme): string { export function userThemeLabel(theme: Theme): string {
return nls.localize('arduino/theme/user', '{0} (user)', theme.label); return nls.localize('arduino/theme/user', '{0} (user)', theme.label);
} }
@ -57,6 +64,8 @@ export function themeLabelForSettings(theme: Theme): string {
return darkThemeLabel; return darkThemeLabel;
case BuiltinThemeProvider.hcTheme.id: case BuiltinThemeProvider.hcTheme.id:
return hcThemeLabel; return hcThemeLabel;
case BuiltinThemeProvider.hcLightTheme.id:
return hcLightThemeLabel;
case BuiltinThemeProvider.lightTheme.id: // fall-through case BuiltinThemeProvider.lightTheme.id: // fall-through
case BuiltinThemeProvider.darkTheme.id: case BuiltinThemeProvider.darkTheme.id:
return deprecatedThemeLabel(theme); return deprecatedThemeLabel(theme);
@ -73,6 +82,8 @@ export function compatibleBuiltInTheme(theme: Theme): Theme {
return ArduinoThemes.dark; return ArduinoThemes.dark;
case 'hc': case 'hc':
return BuiltinThemeProvider.hcTheme; return BuiltinThemeProvider.hcTheme;
case 'hcLight':
return BuiltinThemeProvider.hcLightTheme;
default: { default: {
console.warn( console.warn(
`Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}` `Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}`
@ -91,7 +102,7 @@ interface ThemeProvider {
/** /**
* Returns with a list of built-in themes officially supported by IDE2 (https://github.com/arduino/arduino-ide/issues/1283). * Returns with a list of built-in themes officially supported by IDE2 (https://github.com/arduino/arduino-ide/issues/1283).
* The themes in the array follow the following order: * The themes in the array follow the following order:
* - built-in themes first (in `Light`, `Dark`, `High Contrast`), // TODO -> High Contrast will be split up to HC Dark and HC Light after the Theia version uplift * - built-in themes first (in `Light`, `Dark`, `Light High Contrast`, and `Dark High Contrast`),
* - followed by user installed (VSIX) themes grouped by theme type, then alphabetical order, * - followed by user installed (VSIX) themes grouped by theme type, then alphabetical order,
* - if the `currentTheme` is either Light (Theia) or Dark (Theia), the last item of the array will be the selected theme with `(deprecated)` suffix. * - if the `currentTheme` is either Light (Theia) or Dark (Theia), the last item of the array will be the selected theme with `(deprecated)` suffix.
*/ */
@ -171,7 +182,8 @@ const arduinoThemeTypeOrder: Record<ArduinoThemeType, number> = {
const themeTypeOrder: Record<ThemeType, number> = { const themeTypeOrder: Record<ThemeType, number> = {
light: 0, light: 0,
dark: 1, dark: 1,
hc: 2, hcLight: 2,
hc: 3,
}; };
export function arduinoThemeTypeOf(theme: Theme | string): ArduinoThemeType { export function arduinoThemeTypeOf(theme: Theme | string): ArduinoThemeType {

View File

@ -1,4 +1,4 @@
import type { StartupTask } from '../../../electron-common/startup-task'; import type { StartupTasks } from '../../../electron-common/startup-task';
export const WindowServiceExt = Symbol('WindowServiceExt'); export const WindowServiceExt = Symbol('WindowServiceExt');
export interface WindowServiceExt { export interface WindowServiceExt {
@ -6,5 +6,6 @@ export interface WindowServiceExt {
* Returns with a promise that resolves to `true` if the current window is the first window. * Returns with a promise that resolves to `true` if the current window is the first window.
*/ */
isFirstWindow(): Promise<boolean>; isFirstWindow(): Promise<boolean>;
reload(options?: StartupTask.Owner): void; reload(tasks?: StartupTasks): void;
close(): void;
} }

View File

@ -1,4 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types'; import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
@ -118,10 +117,8 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
if (widget instanceof EditorWidget) { if (widget instanceof EditorWidget) {
const { uri } = widget.editor; const { uri } = widget.editor;
const filename = uri.path.toString(); 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) { if (this.previousRepresentedFilename !== filename) {
const currentWindow = remote.getCurrentWindow(); window.electronArduino.setRepresentedFilename(uri.path.fsPath());
currentWindow.setRepresentedFilename(uri.path.toString());
this.previousRepresentedFilename = filename; this.previousRepresentedFilename = filename;
} }
} }

View File

@ -1,29 +0,0 @@
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 (
<span
tabIndex={0}
className={classNames.join(' ')}
title={label}
onClick={this.props.run}
ref={this.setRef}
>
{!iconClass ||
(iconClass.match(/plugin-icon-\d+/) && <div>{label}</div>)}
</span>
);
}
}

View File

@ -1,4 +1,4 @@
import debounce = require('p-debounce'); import debounce from 'p-debounce';
import { import {
inject, inject,
injectable, injectable,

View File

@ -43,7 +43,8 @@ export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory
this.messages, this.messages,
this.fileService, this.fileService,
this.debugContributionProvider, this.debugContributionProvider,
this.workspaceService this.workspaceService,
2_000
); );
} }
} }

View File

@ -1,120 +0,0 @@
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 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 {
protected debugStateKey: ContextKey<string>;
@postConstruct()
protected override init(): void {
this.debugStateKey = this.contextKeyService.createKey<string>(
'debugState',
debugStateLabel(this.state)
);
super.init();
}
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);
}
protected override async doStart(
sessionId: string,
options: DebugConfigurationSessionOptions
): Promise<DebugSession> {
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);
this.debugTypeKey.set(session.configuration.type);
// this.onDidCreateDebugSessionEmitter.fire(session); // defer the didCreate event after start https://github.com/eclipse-theia/theia/issues/11916
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 })
);
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;
}
}

View File

@ -1,231 +1,12 @@
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 { 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 { export class DebugSession extends TheiaDebugSession {
/** // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
* The `send('initialize')` request resolves later than `on('initialized')` emits the event. protected override handleDisconnectError(err: unknown): void {
* Hence, the `configure` would use the empty object `capabilities`. // NOOP
* Using the empty `capabilities` could result in missing exception breakpoint filters, as }
* always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
* around this timing issue. protected override handleTerminateError(err: unknown): void {
* See: https://github.com/eclipse-theia/theia/issues/11886. // NOOP
*/
protected didReceiveCapabilities = new Deferred();
protected override async initialize(): Promise<void> {
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<void> {
await this.didReceiveCapabilities.promise;
return super.configure();
}
override async stop(isRestart: boolean, callback: () => void): Promise<void> {
// 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<void> {
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<void> {
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<Mutable<DebugThreadData>> = { 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);
} }
} }

View File

@ -1,32 +0,0 @@
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<EditorWidget | undefined> {
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,
});
}
}

View File

@ -1,22 +0,0 @@
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<TheiaDebugStackFrame>();
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()];
}
}

View File

@ -1,85 +0,0 @@
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 (
<React.Fragment>
{this.renderContributedCommands()}
{this.renderContinue()}
<DebugAction
enabled={state === DebugState.Stopped}
run={this.stepOver}
label={nls.localizeByDefault('Step Over')}
iconClass="debug-step-over"
ref={this.setStepRef}
/>
<DebugAction
enabled={state === DebugState.Stopped}
run={this.stepIn}
label={nls.localizeByDefault('Step Into')}
iconClass="debug-step-into"
/>
<DebugAction
enabled={state === DebugState.Stopped}
run={this.stepOut}
label={nls.localizeByDefault('Step Out')}
iconClass="debug-step-out"
/>
<DebugAction
enabled={state !== DebugState.Inactive}
run={this.restart}
label={nls.localizeByDefault('Restart')}
iconClass="debug-restart"
/>
{this.renderStart()}
</React.Fragment>
);
}
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 && (
<DebugAction
key={command}
enabled={enabled}
label={label}
iconClass={iconClass}
run={run}
/>
)
);
}
}

View File

@ -1,5 +1,3 @@
import { ResourceSaveOptions } from '@theia/core/lib/common/resource';
import { Readable } from '@theia/core/lib/common/stream';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { import {
@ -11,14 +9,13 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { import {
FileOperationError, FileOperationError,
FileOperationResult, FileOperationResult,
FileStat,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
import * as PQueue from 'p-queue'; import PQueue from 'p-queue';
@injectable() @injectable()
export class FileResourceResolver extends TheiaFileResourceResolver { export class FileResourceResolver extends TheiaFileResourceResolver {
override async resolve(uri: URI): Promise<WriteQueuedFileResource> { override async resolve(uri: URI): Promise<WriteQueuedFileResource> {
let stat: FileStat | undefined; let stat;
try { try {
stat = await this.fileService.resolve(uri); stat = await this.fileService.resolve(uri);
} catch (e) { } catch (e) {
@ -37,6 +34,7 @@ export class FileResourceResolver extends TheiaFileResourceResolver {
); );
} }
return new WriteQueuedFileResource(uri, this.fileService, { return new WriteQueuedFileResource(uri, this.fileService, {
isReadonly: stat?.isReadonly ?? false,
shouldOverwrite: () => this.shouldOverwrite(uri), shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error), shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error),
}); });
@ -52,23 +50,32 @@ class WriteQueuedFileResource extends FileResource {
options: FileResourceOptions options: FileResourceOptions
) { ) {
super(uri, fileService, options); super(uri, fileService, options);
const originalDoWrite = this['doWrite'];
this['doWrite'] = (content, options) =>
this.writeQueue.add(() => originalDoWrite.bind(this)(content, options));
const originalSaveStream = this['saveStream'];
if (originalSaveStream) {
this['saveStream'] = (content, options) =>
this.writeQueue.add(() =>
originalSaveStream.bind(this)(content, options)
);
}
const originalSaveContents = this['saveContents'];
if (originalSaveContents) {
this['saveContents'] = (content, options) =>
this.writeQueue.add(() =>
originalSaveContents.bind(this)(content, options)
);
}
const originalSaveContentChanges = this['saveContentChanges']; const originalSaveContentChanges = this['saveContentChanges'];
if (originalSaveContentChanges) { if (originalSaveContentChanges) {
this['saveContentChanges'] = (changes, options) => { this['saveContentChanges'] = (changes, options) =>
return this.writeQueue.add(() => this.writeQueue.add(() =>
originalSaveContentChanges.bind(this)(changes, options) originalSaveContentChanges.bind(this)(changes, options)
); );
};
} }
} }
protected override async doWrite(
content: string | Readable<string>,
options?: ResourceSaveOptions
): Promise<void> {
return this.writeQueue.add(() => super.doWrite(content, options));
}
protected override async isInSync(): Promise<boolean> { protected override async isInSync(): Promise<boolean> {
// Let all the write operations finish to update the version (mtime) before checking whether the resource is in sync. // Let all the write operations finish to update the version (mtime) before checking whether the resource is in sync.
// https://github.com/eclipse-theia/theia/issues/12327 // https://github.com/eclipse-theia/theia/issues/12327

View File

@ -8,7 +8,7 @@ import URI from '@theia/core/lib/common/uri';
import { Marker } from '@theia/markers/lib/common/marker'; import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager'; import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigServiceClient } from '../../config/config-service-client'; import { ConfigServiceClient } from '../../config/config-service-client';
import debounce = require('lodash.debounce'); import debounce from 'lodash.debounce';
import { import {
ARDUINO_CLOUD_FOLDER, ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER, REMOTE_SKETCHBOOK_FOLDER,

View File

@ -0,0 +1,24 @@
import type { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import { injectable } from '@theia/core/shared/inversify';
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
@injectable()
export class MonacoEditorMenuContribution extends TheiaMonacoEditorMenuContribution {
override registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
// https://github.com/arduino/arduino-ide/issues/1394
registry.unregisterMenuAction('editor.action.refactor'); // Refactor...
registry.unregisterMenuAction('editor.action.sourceAction'); // Source Action...
// https://github.com/arduino/arduino-ide/pull/2027#pullrequestreview-1414246614
// Root editor context menu
registry.unregisterMenuAction('editor.action.revealDeclaration'); // Go to Declaration
registry.unregisterMenuAction('editor.action.goToTypeDefinition'); // Go to Type Definition
registry.unregisterMenuAction('editor.action.goToImplementation'); // Go to Implementations
registry.unregisterMenuAction('editor.action.goToReferences'); // Go to References
// Peek submenu
registry.unregisterMenuAction('editor.action.peekDeclaration'); // Peek Declaration
registry.unregisterMenuAction('editor.action.peekTypeDefinition'); // Peek Type Definition
registry.unregisterMenuAction('editor.action.peekImplementation'); // Peek Implementation
registry.unregisterMenuAction('editor.action.referenceSearch.trigger'); // Peek References
}
}

View File

@ -1,66 +0,0 @@
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<void> {
const debugType = description.type;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _this = <any>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))
);
}
}

View File

@ -1,16 +1,7 @@
import { Emitter, Event, JsonRpcProxy } from '@theia/core'; import { Emitter, Event, JsonRpcProxy } from '@theia/core';
import { injectable, interfaces } from '@theia/core/shared/inversify'; import { injectable, interfaces } from '@theia/core/shared/inversify';
import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol'; import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
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() @injectable()
export class HostedPluginSupport extends TheiaHostedPluginSupport { export class HostedPluginSupport extends TheiaHostedPluginSupport {
@ -41,26 +32,4 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any).server; 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);
}
} }

View File

@ -1,37 +0,0 @@
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
);
}
}

View File

@ -1,62 +0,0 @@
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<DebugContribution>,
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<TerminalWidget> {
terminalWidgetOptions = Object.assign(
{},
terminalWidgetOptions,
this.terminalOptionsExt
);
return super.doCreateTerminal(terminalWidgetOptions);
}
}

View File

@ -1,73 +0,0 @@
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<string, MenuPath[]>;
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)]
: [];
(<Array<[ContributionPoint, ArgumentAdapter | undefined]>>[
['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);
}
}

View File

@ -15,8 +15,9 @@ import {
SketchesService, SketchesService,
} from '../../../common/protocol/sketches-service'; } from '../../../common/protocol/sketches-service';
import { import {
StartupTask,
StartupTaskProvider, StartupTaskProvider,
hasStartupTasks,
StartupTask,
} from '../../../electron-common/startup-task'; } from '../../../electron-common/startup-task';
import { WindowServiceExt } from '../core/window-service-ext'; import { WindowServiceExt } from '../core/window-service-ext';
@ -128,7 +129,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
.getContributions() .getContributions()
.map((contribution) => contribution.tasks()) .map((contribution) => contribution.tasks())
.reduce((prev, curr) => prev.concat(curr), []); .reduce((prev, curr) => prev.concat(curr), []);
if (StartupTask.has(options)) { if (hasStartupTasks(options)) {
tasks.push(...options.tasks); tasks.push(...options.tasks);
} }
return tasks; return tasks;

View File

@ -1,4 +1,4 @@
import { notEmpty } from '@theia/core'; import { notEmpty } from '@theia/core/lib/common/objects';
/** /**
* Finds the closest child HTMLButtonElement representing a Theia button. * Finds the closest child HTMLButtonElement representing a Theia button.

View File

@ -1,16 +1,9 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import type { Props, StylesConfig, ThemeConfig } from 'react-select';
import Select from 'react-select'; import Select from 'react-select';
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< export class ArduinoSelect extends React.Component<Props> {
Option, constructor(props: Props) {
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
> extends React.Component<StateManagerProps<Option, IsMulti, Group>> {
constructor(props: StateManagerProps<Option, IsMulti, Group>) {
super(props); super(props);
} }

View File

@ -12,8 +12,8 @@ import { AuthenticationClientService } from '../../auth/authentication-client-se
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget'; import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
import { CreateNew } from '../sketchbook/create-new'; import { CreateNew } from '../sketchbook/create-new';
import { AuthenticationSession } from '../../../node/auth/types';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
import { AuthenticationSession } from '../../../common/protocol/authentication-service';
@injectable() @injectable()
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> { export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {

View File

@ -34,6 +34,7 @@ import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
import { CloudSketchbookCommands } from './cloud-sketchbook-commands'; import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
import { CloudSketchbookTree } from './cloud-sketchbook-tree'; import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CreateUri } from '../../create/create-uri'; import { CreateUri } from '../../create/create-uri';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context']; const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
@ -61,6 +62,8 @@ export class CloudSketchbookContribution extends CloudSketchContribution {
private readonly configServiceClient: ConfigServiceClient; private readonly configServiceClient: ConfigServiceClient;
@inject(ApplicationConnectionStatusContribution) @inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution; private readonly connectionStatus: ApplicationConnectionStatusContribution;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
private readonly onDidChangeToolbarEmitter = new Emitter<void>(); private readonly onDidChangeToolbarEmitter = new Emitter<void>();
private readonly toDisposeBeforeNewContextMenu = new DisposableCollection(); private readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
@ -176,6 +179,7 @@ export class CloudSketchbookContribution extends CloudSketchContribution {
node: arg.node, node: arg.node,
title: nls.localize('arduino/cloud/shareSketch', 'Share Sketch'), title: nls.localize('arduino/cloud/shareSketch', 'Share Sketch'),
createApi: this.createApi, createApi: this.createApi,
clipboardService: this.clipboardService,
}).open(); }).open();
}, },
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg), isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),

View File

@ -35,6 +35,7 @@ function sketchBaseDir(sketch: Create.Sketch): FileStat {
isDirectory: true, isDirectory: true,
isFile: false, isFile: false,
isSymbolicLink: false, isSymbolicLink: false,
isReadonly: false,
resource: createPath, resource: createPath,
mtime, mtime,
ctime, ctime,

View File

@ -12,7 +12,6 @@ import {
import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget'; import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget';
import { TreeNode } from '@theia/core/lib/browser/tree'; import { TreeNode } from '@theia/core/lib/browser/tree';
import { CompositeTreeNode } from '@theia/core/lib/browser'; import { CompositeTreeNode } from '@theia/core/lib/browser';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget'; import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
@ -60,7 +59,12 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
</div> </div>
<button <button
className="theia-button uppercase" className="theia-button uppercase"
onClick={() => shell.openExternal('https://create.arduino.cc/editor')} onClick={() =>
this.windowService.openNewWindow(
'https://create.arduino.cc/editor',
{ external: true }
)
}
> >
{nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')} {nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')}
</button> </button>
@ -81,7 +85,10 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
return CompositeTreeNode.is(node) && node.children.length === 0; return CompositeTreeNode.is(node) && node.children.length === 0;
} }
protected override createNodeClassNames(node: any, props: NodeProps): string[] { protected override createNodeClassNames(
node: any,
props: NodeProps
): string[] {
const classNames = super.createNodeClassNames(node, props); const classNames = super.createNodeClassNames(node, props);
if ( if (

View File

@ -1,5 +1,5 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import debounce = require('lodash.debounce'); import debounce from 'lodash.debounce';
import { Event } from '@theia/core/lib/common/event'; import { Event } from '@theia/core/lib/common/event';
import { CommandService } from '@theia/core/lib/common/command'; import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';

View File

@ -1,4 +1,3 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command'; import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
@ -120,7 +119,7 @@ export class SketchbookWidgetContribution
if (exists) { if (exists) {
const fsPath = await this.fileService.fsPath(new URI(arg.node.uri)); const fsPath = await this.fileService.fsPath(new URI(arg.node.uri));
if (fsPath) { if (fsPath) {
remote.shell.openPath(fsPath); window.electronArduino.openPath(fsPath);
} }
} }
} }

View File

@ -1,2 +0,0 @@
export const SHOW_PLOTTER_WINDOW = 'SHOW_PLOTTER_WINDOW';
export const CLOSE_PLOTTER_WINDOW = 'CLOSE_PLOTTER_WINDOW';

View File

@ -1,5 +1,15 @@
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { AuthOptions } from '../../node/auth/types'; export const authServerPort = 9876;
export interface AuthOptions {
redirectUri: string;
responseType: string;
clientID: string;
domain: string;
audience: string;
registerUri: string;
scopes: string[];
}
export interface AuthenticationSession { export interface AuthenticationSession {
readonly id: string; readonly id: string;

View File

@ -1,8 +1,12 @@
import * as semver from 'semver'; import type { MessageService } from '@theia/core/lib/common/message-service';
import { ExecuteWithProgress } from './progressible'; import {
coerce as coerceSemver,
compare as compareSemver,
parse as parseSemver,
} from 'semver';
import { naturalCompare } from '../utils'; import { naturalCompare } from '../utils';
import type { ArduinoComponent } from './arduino-component'; import type { ArduinoComponent } from './arduino-component';
import type { MessageService } from '@theia/core/lib/common/message-service'; import { ExecuteWithProgress } from './progressible';
import type { ResponseServiceClient } from './response-service'; import type { ResponseServiceClient } from './response-service';
export interface Installable<T extends ArduinoComponent> { export interface Installable<T extends ArduinoComponent> {
@ -35,16 +39,16 @@ export namespace Installable {
right: Version, right: Version,
coerce = false coerce = false
): number => { ): number => {
const validLeft = semver.parse(left); const validLeft = parseSemver(left);
const validRight = semver.parse(right); const validRight = parseSemver(right);
if (validLeft && validRight) { if (validLeft && validRight) {
return semver.compare(validLeft, validRight); return compareSemver(validLeft, validRight);
} }
if (coerce) { if (coerce) {
const coercedLeft = validLeft ?? semver.coerce(left); const coercedLeft = validLeft ?? coerceSemver(left);
const coercedRight = validRight ?? semver.coerce(right); const coercedRight = validRight ?? coerceSemver(right);
if (coercedLeft && coercedRight) { if (coercedLeft && coercedRight) {
return semver.compare(coercedLeft, coercedRight); return compareSemver(coercedLeft, coercedRight);
} }
} }
return naturalCompare(left, right); return naturalCompare(left, right);

View File

@ -1,10 +1,12 @@
import { ApplicationError, Event, JsonRpcServer, nls } from '@theia/core'; import { ApplicationError, Event, JsonRpcServer, nls } from '@theia/core';
import {
PluggableMonitorSettings,
MonitorSettings,
} from '../../node/monitor-settings/monitor-settings-provider';
import { Board, Port } from './boards-service'; import { Board, Port } from './boards-service';
export type PluggableMonitorSettings = Record<string, PluggableMonitorSetting>;
export interface MonitorSettings {
pluggableMonitorSettings?: PluggableMonitorSettings;
monitorUISettings?: Partial<MonitorState>;
}
export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory'); export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory');
export type MonitorManagerProxyFactory = () => MonitorManagerProxy; export type MonitorManagerProxyFactory = () => MonitorManagerProxy;

View File

@ -1,7 +1,7 @@
import { ApplicationError } from '@theia/core/lib/common/application-error'; import { ApplicationError } from '@theia/core/lib/common/application-error';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import * as dateFormat from 'dateformat'; import dateFormat from 'dateformat';
const filenameReservedRegex = require('filename-reserved-regex'); const filenameReservedRegex = require('filename-reserved-regex');
export namespace SketchesError { export namespace SketchesError {

View File

@ -0,0 +1,26 @@
import type { Disposable } from '@theia/core/lib/common/disposable';
import { injectable } from '@theia/core/shared/inversify';
import type { AppService } from '../browser/app-service';
import type { Sketch } from '../common/protocol/sketches-service';
import type { StartupTasks } from '../electron-common/startup-task';
@injectable()
export class ElectronAppService implements AppService {
quit(): void {
window.electronArduino.quitApp();
}
version(): Promise<string> {
return window.electronArduino.appVersion();
}
registerStartupTasksHandler(
handler: (tasks: StartupTasks) => void
): Disposable {
return window.electronArduino.registerStartupTasksHandler(handler);
}
scheduleDeletion(sketch: Sketch): void {
window.electronArduino.scheduleDeletion(sketch);
}
}

View File

@ -0,0 +1,12 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { AppService } from '../browser/app-service';
import { DialogService } from '../browser/dialog-service';
import { ElectronAppService } from './electron-app-service';
import { ElectronDialogService } from './electron-dialog-service';
export default new ContainerModule((bind) => {
bind(ElectronAppService).toSelf().inSingletonScope();
bind(AppService).toService(ElectronAppService);
bind(ElectronDialogService).toSelf().inSingletonScope();
bind(DialogService).toService(ElectronDialogService);
});

View File

@ -0,0 +1,25 @@
import { injectable } from '@theia/core/shared/inversify';
import type { DialogService } from '../browser/dialog-service';
import type {
MessageBoxOptions,
MessageBoxReturnValue,
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue,
} from '../electron-common/electron-arduino';
@injectable()
export class ElectronDialogService implements DialogService {
showMessageBox(options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
return window.electronArduino.showMessageBox(options);
}
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnValue> {
return window.electronArduino.showOpenDialog(options);
}
showSaveDialog(options: SaveDialogOptions): Promise<SaveDialogReturnValue> {
return window.electronArduino.showSaveDialog(options);
}
}

View File

@ -0,0 +1,131 @@
import {
contextBridge,
ipcRenderer,
} from '@theia/core/electron-shared/electron';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
CHANNEL_REQUEST_RELOAD,
MenuDto,
} from '@theia/core/lib/electron-common/electron-api';
import { v4 } from 'uuid';
import type { Sketch } from '../common/protocol/sketches-service';
import {
CHANNEL_APP_VERSION,
CHANNEL_IS_FIRST_WINDOW,
CHANNEL_MAIN_MENU_ITEM_DID_CLICK,
CHANNEL_OPEN_PATH,
CHANNEL_PLOTTER_WINDOW_DID_CLOSE,
CHANNEL_QUIT_APP,
CHANNEL_SCHEDULE_DELETION,
CHANNEL_SEND_STARTUP_TASKS,
CHANNEL_SET_MENU_WITH_NODE_ID,
CHANNEL_SET_REPRESENTED_FILENAME,
CHANNEL_SHOW_MESSAGE_BOX,
CHANNEL_SHOW_OPEN_DIALOG,
CHANNEL_SHOW_PLOTTER_WINDOW,
CHANNEL_SHOW_SAVE_DIALOG,
ElectronArduino,
InternalMenuDto,
MessageBoxOptions,
OpenDialogOptions,
SaveDialogOptions,
} from '../electron-common/electron-arduino';
import { hasStartupTasks, StartupTasks } from '../electron-common/startup-task';
let mainMenuHandlers: Map<string, () => void> = new Map();
function convertMenu(
menu: MenuDto[] | undefined,
handlerMap: Map<string, () => void>
): InternalMenuDto[] | undefined {
if (!menu) {
return undefined;
}
return menu.map((item) => {
let nodeId = v4();
if (item.execute) {
if (!item.id) {
throw new Error(
"A menu item having the 'execute' property must have an 'id' too."
);
}
nodeId = item.id;
handlerMap.set(nodeId, item.execute);
}
return {
id: item.id,
submenu: convertMenu(item.submenu, handlerMap),
accelerator: item.accelerator,
label: item.label,
nodeId,
checked: item.checked,
enabled: item.enabled,
role: item.role,
type: item.type,
visible: item.visible,
};
});
}
const api: ElectronArduino = {
showMessageBox: (options: MessageBoxOptions) =>
ipcRenderer.invoke(CHANNEL_SHOW_MESSAGE_BOX, options),
showOpenDialog: (options: OpenDialogOptions) =>
ipcRenderer.invoke(CHANNEL_SHOW_OPEN_DIALOG, options),
showSaveDialog: (options: SaveDialogOptions) =>
ipcRenderer.invoke(CHANNEL_SHOW_SAVE_DIALOG, options),
appVersion: () => ipcRenderer.invoke(CHANNEL_APP_VERSION),
quitApp: () => ipcRenderer.send(CHANNEL_QUIT_APP),
isFirstWindow: () => ipcRenderer.invoke(CHANNEL_IS_FIRST_WINDOW),
requestReload: (options: StartupTasks) =>
ipcRenderer.send(CHANNEL_REQUEST_RELOAD, options),
registerStartupTasksHandler: (handler: (tasks: StartupTasks) => void) => {
const listener = (_: Electron.IpcRendererEvent, args: unknown) => {
if (hasStartupTasks(args)) {
handler(args);
} else {
console.warn(
`Events received on the ${CHANNEL_SEND_STARTUP_TASKS} channel expected to have a startup task argument, but it was: ${JSON.stringify(
args
)}`
);
}
};
ipcRenderer.on(CHANNEL_SEND_STARTUP_TASKS, listener);
return Disposable.create(() =>
ipcRenderer.removeListener(CHANNEL_SEND_STARTUP_TASKS, listener)
);
},
scheduleDeletion: (sketch: Sketch) =>
ipcRenderer.send(CHANNEL_SCHEDULE_DELETION, sketch),
setRepresentedFilename: (fsPath: string) =>
ipcRenderer.send(CHANNEL_SET_REPRESENTED_FILENAME, fsPath),
showPlotterWindow: (params: { url: string; forceReload?: boolean }) =>
ipcRenderer.send(CHANNEL_SHOW_PLOTTER_WINDOW, params),
registerPlotterWindowCloseHandler: (handler: () => void) => {
const listener = () => handler();
ipcRenderer.on(CHANNEL_PLOTTER_WINDOW_DID_CLOSE, listener);
return Disposable.create(() =>
ipcRenderer.removeListener(CHANNEL_PLOTTER_WINDOW_DID_CLOSE, listener)
);
},
openPath: (fsPath: string) => ipcRenderer.send(CHANNEL_OPEN_PATH, fsPath),
setMenu: (menu: MenuDto[] | undefined): void => {
mainMenuHandlers = new Map();
const internalMenu = convertMenu(menu, mainMenuHandlers);
ipcRenderer.send(CHANNEL_SET_MENU_WITH_NODE_ID, internalMenu);
},
};
export function preload(): void {
contextBridge.exposeInMainWorld('electronArduino', api);
ipcRenderer.on(CHANNEL_MAIN_MENU_ITEM_DID_CLICK, (_, nodeId: string) => {
const handler = mainMenuHandlers.get(nodeId);
if (handler) {
handler();
}
});
console.log('Exposed Arduino IDE electron API');
}

View File

@ -1,4 +1,3 @@
import { webFrame } from '@theia/core/electron-shared/electron/';
import { import {
ContextMenuAccess, ContextMenuAccess,
coordinateFromAnchor, coordinateFromAnchor,
@ -16,28 +15,24 @@ export class ElectronContextMenuRenderer extends TheiaElectronContextMenuRendere
options: RenderContextMenuOptions options: RenderContextMenuOptions
): ContextMenuAccess { ): ContextMenuAccess {
if (this.useNativeStyle) { if (this.useNativeStyle) {
const { menuPath, anchor, args, onHide, context } = options; const { menuPath, anchor, args, onHide, context, contextKeyService } =
options;
const menu = this['electronMenuFactory'].createElectronContextMenu( const menu = this['electronMenuFactory'].createElectronContextMenu(
menuPath, menuPath,
args, args,
context, context,
contextKeyService,
this.showDisabled(options) this.showDisabled(options)
); );
const { x, y } = coordinateFromAnchor(anchor); const { x, y } = coordinateFromAnchor(anchor);
const zoom = webFrame.getZoomFactor(); const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => {
// TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 if (onHide) {
const offset = process.platform === 'win32' ? 0 : 2; onHide();
// x and y values must be Ints or else there is a conversion error }
menu.popup({
x: Math.round(x * zoom) + offset,
y: Math.round(y * zoom) + offset,
}); });
// native context menu stops the event loop, so there is no keyboard events // native context menu stops the event loop, so there is no keyboard events
this.context.resetAltPressed(); this.context.resetAltPressed();
if (onHide) { return new ElectronContextMenuAccess(menuHandle);
menu.once('menu-will-close', () => onHide());
}
return new ElectronContextMenuAccess(menu);
} else { } else {
return super.doRender(options); return super.doRender(options);
} }

View File

@ -1,4 +1,4 @@
import * as remote from '@theia/core/electron-shared/@electron/remote'; import { ContextMatcher } from '@theia/core/lib/browser/context-key-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { import {
@ -11,15 +11,19 @@ import {
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { import {
ElectronMainMenuFactory as TheiaElectronMainMenuFactory,
ElectronMenuItemRole,
ElectronMenuOptions, ElectronMenuOptions,
ElectronMainMenuFactory as TheiaElectronMainMenuFactory,
} from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import type {
MenuDto,
MenuRole,
} from '@theia/core/lib/electron-common/electron-api';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { import {
ArduinoMenus, ArduinoMenus,
PlaceholderMenuNode, PlaceholderMenuNode,
} from '../../../browser/menu/arduino-menus'; } from '../../../browser/menu/arduino-menus';
import debounce from 'lodash.debounce';
@injectable() @injectable()
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
@ -30,7 +34,27 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
private updateWhenReady = false; private updateWhenReady = false;
override postConstruct(): void { override postConstruct(): void {
super.postConstruct(); // #region Theia `postConstruct` customizations with calling IDE2 `setMenu`
this.preferencesService.onPreferenceChanged(
debounce((e) => {
if (e.preferenceName === 'window.menuBarVisibility') {
this.setMenuBar();
}
if (this._menu) {
for (const cmd of this._toggledCommands) {
const menuItem = this.findMenuById(this._menu, cmd);
if (menuItem) {
menuItem.checked = this.commandRegistry.isToggled(cmd);
}
}
window.electronArduino.setMenu(this._menu); // calls the IDE2-specific implementation
}
}, 10)
);
this.keybindingRegistry.onKeybindingsChanged(() => {
this.setMenuBar();
});
// #endregion Theia `postConstruct`
this.appStateService.reachedState('ready').then(() => { this.appStateService.reachedState('ready').then(() => {
this.appReady = true; this.appReady = true;
if (this.updateWhenReady) { if (this.updateWhenReady) {
@ -39,18 +63,18 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
}); });
} }
override createElectronMenuBar(): Electron.Menu { override createElectronMenuBar(): MenuDto[] {
this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977 this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR);
const template = this.fillMenuTemplate([], menuModel, [], { const menu = this.fillMenuTemplate([], menuModel, [], {
rootMenuPath: MAIN_MENU_BAR, rootMenuPath: MAIN_MENU_BAR,
}); });
if (isOSX) { if (isOSX) {
template.unshift(this.createOSXMenu()); menu.unshift(this.createOSXMenu());
} }
const menu = remote.Menu.buildFromTemplate(this.escapeAmpersand(template)); const escapedMenu = this.escapeAmpersand(menu);
this._menu = menu; this._menu = escapedMenu;
return menu; return escapedMenu;
} }
override async setMenuBar(): Promise<void> { override async setMenuBar(): Promise<void> {
@ -63,11 +87,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
} }
await this.preferencesService.ready; await this.preferencesService.ready;
const createdMenuBar = this.createElectronMenuBar(); const createdMenuBar = this.createElectronMenuBar();
if (isOSX) { window.electronArduino.setMenu(createdMenuBar);
remote.Menu.setApplicationMenu(createdMenuBar);
} else {
remote.getCurrentWindow().setMenu(createdMenuBar);
}
} }
override createElectronContextMenu( override createElectronContextMenu(
@ -75,35 +95,68 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[], args?: any[],
context?: HTMLElement, context?: HTMLElement,
contextKeyService?: ContextMatcher,
showDisabled?: boolean showDisabled?: boolean
): Electron.Menu { ): MenuDto[] {
const menuModel = this.menuProvider.getMenu(menuPath); const menuModel = this.menuProvider.getMenu(menuPath);
const template = this.fillMenuTemplate([], menuModel, args, { return this.fillMenuTemplate([], menuModel, args, {
showDisabled, showDisabled,
context, context,
rootMenuPath: menuPath, rootMenuPath: menuPath,
contextKeyService,
}); });
return remote.Menu.buildFromTemplate(this.escapeAmpersand(template)); }
protected override async execute(
commandId: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any[],
menuPath: MenuPath
): Promise<void> {
try {
// This is workaround for https://github.com/eclipse-theia/theia/issues/446.
// Electron menus do not update based on the `isEnabled`, `isVisible` property of the command.
// We need to check if we can execute it.
if (this.menuCommandExecutor.isEnabled(menuPath, commandId, ...args)) {
await this.menuCommandExecutor.executeCommand(
menuPath,
commandId,
...args
);
if (
this._menu &&
this.menuCommandExecutor.isVisible(menuPath, commandId, ...args)
) {
const item = this.findMenuById(this._menu, commandId);
if (item) {
item.checked = this.menuCommandExecutor.isToggled(
menuPath,
commandId,
...args
);
window.electronArduino.setMenu(this._menu); // overridden to call the IDE2-specific implementation.
}
}
}
} catch {
// no-op
}
} }
// TODO: remove after https://github.com/eclipse-theia/theia/pull/9231 // TODO: remove after https://github.com/eclipse-theia/theia/pull/9231
private escapeAmpersand( private escapeAmpersand(template: MenuDto[]): MenuDto[] {
template: Electron.MenuItemConstructorOptions[]
): Electron.MenuItemConstructorOptions[] {
for (const option of template) { for (const option of template) {
if (option.label) { if (option.label) {
option.label = option.label.replace(/\&+/g, '&$&'); option.label = option.label.replace(/\&+/g, '&$&');
} }
if (option.submenu) { if (option.submenu) {
this.escapeAmpersand( this.escapeAmpersand(option.submenu);
option.submenu as Electron.MenuItemConstructorOptions[]
);
} }
} }
return template; return template;
} }
protected override createOSXMenu(): Electron.MenuItemConstructorOptions { protected override createOSXMenu(): MenuDto {
const { submenu } = super.createOSXMenu(); const { submenu } = super.createOSXMenu();
const label = FrontendApplicationConfigProvider.get().applicationName; const label = FrontendApplicationConfigProvider.get().applicationName;
if (!!submenu && Array.isArray(submenu)) { if (!!submenu && Array.isArray(submenu)) {
@ -142,7 +195,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
protected override roleFor(id: string): ElectronMenuItemRole | undefined { protected override roleFor(id: string): MenuRole | undefined {
// MenuItem `roles` are completely broken on macOS: // MenuItem `roles` are completely broken on macOS:
// - https://github.com/eclipse-theia/theia/issues/11217, // - https://github.com/eclipse-theia/theia/issues/11217,
// - https://github.com/arduino/arduino-ide/issues/969 // - https://github.com/arduino/arduino-ide/issues/969
@ -151,11 +204,11 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
} }
protected override fillMenuTemplate( protected override fillMenuTemplate(
parentItems: Electron.MenuItemConstructorOptions[], parentItems: MenuDto[],
menuModel: MenuNode, menuModel: MenuNode,
args: unknown[] | undefined, args: unknown[] | undefined,
options: ElectronMenuOptions options: ElectronMenuOptions
): Electron.MenuItemConstructorOptions[] { ): MenuDto[] {
if (menuModel instanceof PlaceholderMenuNode) { if (menuModel instanceof PlaceholderMenuNode) {
parentItems.push({ parentItems.push({
label: menuModel.label, label: menuModel.label,
@ -172,24 +225,28 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
// Source: https://github.com/eclipse-theia/theia/blob/5e641750af83383f2ce0cb3432ec333df70778a8/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L132-L203 // Source: https://github.com/eclipse-theia/theia/blob/5e641750af83383f2ce0cb3432ec333df70778a8/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L132-L203
// See https://github.com/arduino/arduino-ide/issues/1533 // See https://github.com/arduino/arduino-ide/issues/1533
private superFillMenuTemplate( private superFillMenuTemplate(
parentItems: Electron.MenuItemConstructorOptions[], parentItems: MenuDto[],
menu: MenuNode, menu: MenuNode,
args: unknown[] = [], args: unknown[] = [],
options: ElectronMenuOptions options: ElectronMenuOptions
): Electron.MenuItemConstructorOptions[] { ): MenuDto[] {
const showDisabled = options?.showDisabled !== false; const showDisabled = options?.showDisabled !== false;
if ( if (
CompoundMenuNode.is(menu) && CompoundMenuNode.is(menu) &&
this.visibleSubmenu(menu) && // customization for #569 and #655 this.visibleSubmenu(menu) && // customization for #569 and #655
this.undefinedOrMatch(menu.when, options.context) this.undefinedOrMatch(
options.contextKeyService ?? this.contextKeyService,
menu.when,
options.context
)
) { ) {
const role = CompoundMenuNode.getRole(menu); const role = CompoundMenuNode.getRole(menu);
if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') { if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') {
return parentItems; return parentItems;
} }
const children = CompoundMenuNode.getFlatChildren(menu.children); const children = CompoundMenuNode.getFlatChildren(menu.children);
const myItems: Electron.MenuItemConstructorOptions[] = []; const myItems: MenuDto[] = [];
children.forEach((child) => children.forEach((child) =>
this.fillMenuTemplate(myItems, child, args, options) this.fillMenuTemplate(myItems, child, args, options)
); );
@ -236,7 +293,11 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
commandId, commandId,
...args ...args
) || ) ||
!this.undefinedOrMatch(node.when, options.context) !this.undefinedOrMatch(
options.contextKeyService ?? this.contextKeyService,
node.when,
options.context
)
) { ) {
return parentItems; return parentItems;
} }
@ -258,7 +319,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
const accelerator = bindings[0] && this.acceleratorFor(bindings[0]); const accelerator = bindings[0] && this.acceleratorFor(bindings[0]);
const menuItem: Electron.MenuItemConstructorOptions = { const menuItem: MenuDto = {
id: node.id, id: node.id,
label: node.label, label: node.label,
type: this.commandRegistry.getToggledHandler(commandId, ...args) type: this.commandRegistry.getToggledHandler(commandId, ...args)
@ -268,14 +329,14 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
enabled: this.commandRegistry.isEnabled(commandId, ...args), // Unlike Theia https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L183 enabled: this.commandRegistry.isEnabled(commandId, ...args), // Unlike Theia https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L183
visible: true, visible: true,
accelerator, accelerator,
click: () => this.execute(commandId, args, options.rootMenuPath), execute: () => this.execute(commandId, args, options.rootMenuPath),
}; };
if (isOSX) { if (isOSX) {
const role = this.roleFor(node.id); const role = this.roleFor(node.id);
if (role) { if (role) {
menuItem.role = role; menuItem.role = role;
delete menuItem.click; delete menuItem.execute;
} }
} }
parentItems.push(menuItem); parentItems.push(menuItem);

View File

@ -1,57 +1,40 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import type { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { CommandRegistry } from '@theia/core/lib/common/command'; import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import type { CommandRegistry } from '@theia/core/lib/common/command';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import type { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { isOSX } from '@theia/core/lib/common/os';
import { import {
ElectronMenuContribution as TheiaElectronMenuContribution,
ElectronCommands, ElectronCommands,
ElectronMenuContribution as TheiaElectronMenuContribution,
} from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'; } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { MainMenuManager } from '../../../common/main-menu-manager'; import type { MenuDto } from '@theia/core/lib/electron-common/electron-api';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { inject, injectable } from '@theia/core/shared/inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import type { MainMenuManager } from '../../../common/main-menu-manager';
import { ZoomLevel } from '@theia/core/lib/electron-browser/window/electron-window-preferences'; import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { PreferenceScope } from '@theia/core/lib/browser/preferences/preference-scope';
import {
getCurrentWindow,
getCurrentWebContents,
} from '@theia/core/electron-shared/@electron/remote';
@injectable() @injectable()
export class ElectronMenuContribution export class ElectronMenuUpdater implements MainMenuManager {
extends TheiaElectronMenuContribution @inject(ElectronMainMenuFactory)
implements MainMenuManager protected readonly factory: ElectronMainMenuFactory;
{
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
// private appReady = false; public update(): void {
// private updateWhenReady = false; this.setMenu();
override onStart(app: FrontendApplication): void {
super.onStart(app);
this.appStateService.reachedState('ready').then(() => {
// this.appReady = true;
// if (this.updateWhenReady) {
// this.update();
// }
});
} }
private setMenu(): void {
window.electronArduino.setMenu(this.factory.createElectronMenuBar());
}
}
@injectable()
export class ElectronMenuContribution extends TheiaElectronMenuContribution {
protected override hideTopPanel(): void { protected override hideTopPanel(): void {
// NOOP // NOOP
// We reuse the `div` for the Arduino toolbar. // We reuse the `div` for the Arduino toolbar.
} }
update(): void {
// if (this.appReady) {
(this as any).setMenu();
// } else {
// this.updateWhenReady = true;
// }
}
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
this.theiaRegisterCommands(registry); super.registerCommands(registry);
registry.unregisterCommand(ElectronCommands.CLOSE_WINDOW); registry.unregisterCommand(ElectronCommands.CLOSE_WINDOW);
} }
@ -67,80 +50,17 @@ export class ElectronMenuContribution
registry.unregisterKeybinding(ElectronCommands.ZOOM_OUT.id); registry.unregisterKeybinding(ElectronCommands.ZOOM_OUT.id);
} }
// Copied from Theia: https://github.com/eclipse-theia/theia/blob/9ec8835cf35d5a46101a62ae93285aeb37a2f382/packages/core/src/electron-browser/menu/electron-menu-contribution.ts#L260-L314 protected override setMenu(
// Unlike the Theia implementation, this does not require synchronously the browser window, but use a function only when the command handler executes. app: FrontendApplication,
private theiaRegisterCommands(registry: CommandRegistry): void { electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()
const currentWindow = () => getCurrentWindow(); ): void {
if (!isOSX) {
registry.registerCommand(ElectronCommands.TOGGLE_DEVELOPER_TOOLS, { this.hideTopPanel(); // no app args. the overridden method is noop in IDE2.
execute: () => { if (this.titleBarStyle === 'custom' && !this.menuBar) {
const webContent = getCurrentWebContents(); this.createCustomTitleBar(app);
if (!webContent.isDevToolsOpened()) {
webContent.openDevTools();
} else {
webContent.closeDevTools();
}
},
});
registry.registerCommand(ElectronCommands.RELOAD, {
execute: () => this.windowService.reload(),
});
registry.registerCommand(ElectronCommands.CLOSE_WINDOW, {
execute: () => currentWindow().close(),
});
registry.registerCommand(ElectronCommands.ZOOM_IN, {
execute: () => {
const webContents = currentWindow().webContents;
// When starting at a level that is not a multiple of 0.5, increment by at most 0.5 to reach the next highest multiple of 0.5.
let zoomLevel =
Math.floor(webContents.zoomLevel / ZoomLevel.VARIATION) *
ZoomLevel.VARIATION +
ZoomLevel.VARIATION;
if (zoomLevel > ZoomLevel.MAX) {
zoomLevel = ZoomLevel.MAX;
return; return;
} }
this.preferenceService.set(
'window.zoomLevel',
zoomLevel,
PreferenceScope.User
);
},
});
registry.registerCommand(ElectronCommands.ZOOM_OUT, {
execute: () => {
const webContents = currentWindow().webContents;
// When starting at a level that is not a multiple of 0.5, decrement by at most 0.5 to reach the next lowest multiple of 0.5.
let zoomLevel =
Math.ceil(webContents.zoomLevel / ZoomLevel.VARIATION) *
ZoomLevel.VARIATION -
ZoomLevel.VARIATION;
if (zoomLevel < ZoomLevel.MIN) {
zoomLevel = ZoomLevel.MIN;
return;
} }
this.preferenceService.set( window.electronArduino.setMenu(electronMenu); // overridden to call the IDE20-specific implementation.
'window.zoomLevel',
zoomLevel,
PreferenceScope.User
);
},
});
registry.registerCommand(ElectronCommands.RESET_ZOOM, {
execute: () =>
this.preferenceService.set(
'window.zoomLevel',
ZoomLevel.DEFAULT,
PreferenceScope.User
),
});
registry.registerCommand(ElectronCommands.TOGGLE_FULL_SCREEN, {
isEnabled: () => currentWindow().isFullScreenable(),
isVisible: () => currentWindow().isFullScreenable(),
execute: () =>
currentWindow().setFullScreen(!currentWindow().isFullScreen()),
});
} }
} }

View File

@ -5,11 +5,15 @@ import { ContainerModule } from '@theia/core/shared/inversify';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
import { ElectronContextMenuRenderer } from './electron-context-menu-renderer'; import { ElectronContextMenuRenderer } from './electron-context-menu-renderer';
import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ElectronMenuContribution } from './electron-menu-contribution'; import {
ElectronMenuContribution,
ElectronMenuUpdater,
} from './electron-menu-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMenuContribution).toSelf().inSingletonScope(); bind(ElectronMenuContribution).toSelf().inSingletonScope();
bind(MainMenuManager).toService(ElectronMenuContribution); bind(ElectronMenuUpdater).toSelf().inSingletonScope();
bind(MainMenuManager).toService(ElectronMenuUpdater);
bind(ElectronContextMenuRenderer).toSelf().inSingletonScope(); bind(ElectronContextMenuRenderer).toSelf().inSingletonScope();
rebind(ContextMenuRenderer).toService(ElectronContextMenuRenderer); rebind(ContextMenuRenderer).toService(ElectronContextMenuRenderer);
rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution); rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution);

View File

@ -1,23 +1,10 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { ContainerModule } from '@theia/core/shared/inversify'; import { ContainerModule } from '@theia/core/shared/inversify';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext'; import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import {
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../../../electron-common/electron-main-window-service-ext';
import { ElectronWindowService } from './electron-window-service'; import { ElectronWindowService } from './electron-window-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronWindowService).toSelf().inSingletonScope(); bind(ElectronWindowService).toSelf().inSingletonScope();
rebind(WindowService).toService(ElectronWindowService); rebind(WindowService).toService(ElectronWindowService);
bind(WindowServiceExt).toService(ElectronWindowService); bind(WindowServiceExt).toService(ElectronWindowService);
bind(ElectronMainWindowServiceExt)
.toDynamicValue(({ container }) =>
ElectronIpcConnectionProvider.createProxy(
container,
electronMainWindowServiceExtPath
)
)
.inSingletonScope();
}); });

View File

@ -1,94 +1,52 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import {
ConnectionStatus,
ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { NewWindowOptions } from '@theia/core/lib/common/window'; import type { NewWindowOptions } from '@theia/core/lib/common/window';
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service'; import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service';
import { RELOAD_REQUESTED_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages'; import { injectable, postConstruct } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext'; import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext'; import {
import { StartupTask } from '../../../electron-common/startup-task'; hasStartupTasks,
StartupTasks,
} from '../../../electron-common/startup-task';
@injectable() @injectable()
export class ElectronWindowService export class ElectronWindowService
extends TheiaElectronWindowService extends TheiaElectronWindowService
implements WindowServiceExt implements WindowServiceExt
{ {
@inject(ConnectionStatusService) private _isFirstWindow: Deferred<boolean> | undefined;
private readonly connectionStatusService: ConnectionStatusService;
@inject(ElectronMainWindowServiceExt)
private readonly mainWindowServiceExt: ElectronMainWindowServiceExt;
@postConstruct() @postConstruct()
protected override init(): void { protected override init(): void {
// NOOP // NOOP
// Does not listen on Theia's `window.zoomLevel` changes. // IDE2 listens on the zoom level changes in `ArduinoFrontendContribution#onStart`
// TODO: IDE2 must switch to the Theia preferences and drop the custom one.
} }
protected shouldUnload(): boolean {
const offline =
this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE;
const detail = offline
? nls.localize(
'arduino/electron/couldNotSave',
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
)
: nls.localize(
'arduino/electron/unsavedChanges',
'Any unsaved changes will not be saved.'
);
const electronWindow = remote.getCurrentWindow();
const response = remote.dialog.showMessageBoxSync(electronWindow, {
type: 'question',
buttons: [
nls.localize('vscode/extensionsUtils/yes', 'Yes'),
nls.localize('vscode/extensionsUtils/no', 'No'),
],
title: nls.localize('vscode/Default/ConfirmTitle', 'Confirm'),
message: nls.localize(
'arduino/sketch/close',
'Are you sure you want to close the sketch?'
),
detail,
});
return response === 0; // 'Yes', close the window.
}
private _firstWindow: Deferred<boolean> | undefined;
async isFirstWindow(): Promise<boolean> { async isFirstWindow(): Promise<boolean> {
if (this._firstWindow === undefined) { if (!this._isFirstWindow) {
this._firstWindow = new Deferred<boolean>(); this._isFirstWindow = new Deferred();
const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE. window.electronArduino
this.mainWindowServiceExt .isFirstWindow()
.isFirstWindow(windowId) .then((isFirstWindow) => this._isFirstWindow?.resolve(isFirstWindow));
.then((firstWindow) => this._firstWindow?.resolve(firstWindow));
} }
return this._firstWindow.promise; return this._isFirstWindow.promise;
} }
// Overridden because the default Theia implementation destroys the additional properties of the `options` arg, such as `tasks`. // Overridden because the default Theia implementation destructures the additional properties of the `options` arg, such as `tasks`.
// https://github.com/eclipse-theia/theia/blob/2deedbad70bd4b503bf9c7e733ab9603f492600f/packages/core/src/electron-browser/window/electron-window-service.ts#L43
override openNewWindow(url: string, options?: NewWindowOptions): undefined { override openNewWindow(url: string, options?: NewWindowOptions): undefined {
return this.delegate.openNewWindow(url, options); return this.delegate.openNewWindow(url, options);
} }
// Overridden to support optional task owner params and make `tsc` happy. // Overridden to support optional task owner params and make `tsc` happy.
override reload(options?: StartupTask.Owner): void { override reload(options?: StartupTasks): void {
if (options?.tasks && options.tasks.length) { if (hasStartupTasks(options)) {
const { tasks } = options; window.electronArduino.requestReload(options);
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL, { tasks });
} else { } else {
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL); window.electronTheiaCore.requestReload();
} }
} }
close(): void {
window.electronTheiaCore.close();
}
} }

View File

@ -0,0 +1,92 @@
import type {
MessageBoxOptions as ElectronMessageBoxOptions,
MessageBoxReturnValue as ElectronMessageBoxReturnValue,
OpenDialogOptions as ElectronOpenDialogOptions,
OpenDialogReturnValue as ElectronOpenDialogReturnValue,
SaveDialogOptions as ElectronSaveDialogOptions,
SaveDialogReturnValue as ElectronSaveDialogReturnValue,
} from '@theia/core/electron-shared/electron';
import type { Disposable } from '@theia/core/lib/common/disposable';
import type {
InternalMenuDto as TheiaInternalMenuDto,
MenuDto,
} from '@theia/core/lib/electron-common/electron-api';
import type { Sketch } from '../common/protocol/sketches-service';
import type { StartupTasks } from './startup-task';
export interface InternalMenuDto
extends Omit<TheiaInternalMenuDto, 'handlerId'> {
// Theia handles the menus with a running-index handler ID. https://github.com/eclipse-theia/theia/issues/12493
// IDE2 keeps the menu `nodeId` instead of the running-index.
nodeId?: string;
}
export type MessageBoxOptions = Omit<
ElectronMessageBoxOptions,
'icon' | 'signal'
>;
export type MessageBoxReturnValue = ElectronMessageBoxReturnValue;
export type OpenDialogOptions = ElectronOpenDialogOptions;
export type OpenDialogReturnValue = ElectronOpenDialogReturnValue;
export type SaveDialogOptions = ElectronSaveDialogOptions;
export type SaveDialogReturnValue = ElectronSaveDialogReturnValue;
export interface ShowPlotterWindowParams {
readonly url: string;
readonly forceReload?: boolean;
}
export function isShowPlotterWindowParams(
arg: unknown
): arg is ShowPlotterWindowParams {
return (
typeof arg === 'object' &&
typeof (<ShowPlotterWindowParams>arg).url === 'string' &&
((<ShowPlotterWindowParams>arg).forceReload === undefined ||
typeof (<ShowPlotterWindowParams>arg).forceReload === 'boolean')
);
}
export interface ElectronArduino {
showMessageBox(options: MessageBoxOptions): Promise<MessageBoxReturnValue>;
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnValue>;
showSaveDialog(options: SaveDialogOptions): Promise<SaveDialogReturnValue>;
appVersion(): Promise<string>;
quitApp(): void;
isFirstWindow(): Promise<boolean>;
requestReload(tasks: StartupTasks): void;
registerStartupTasksHandler(
handler: (tasks: StartupTasks) => void
): Disposable;
scheduleDeletion(sketch: Sketch): void;
setRepresentedFilename(fsPath: string): void;
showPlotterWindow(params: { url: string; forceReload?: boolean }): void;
registerPlotterWindowCloseHandler(handler: () => void): Disposable;
openPath(fsPath: string): void;
// Unlike the Theia implementation, IDE2 uses the command IDs, and not the running-index handler IDs.
// https://github.com/eclipse-theia/theia/issues/12493
setMenu(menu: MenuDto[] | undefined): void;
}
declare global {
interface Window {
electronArduino: ElectronArduino;
}
}
// renderer to main
export const CHANNEL_SHOW_MESSAGE_BOX = 'Arduino:ShowMessageBox';
export const CHANNEL_SHOW_OPEN_DIALOG = 'Arduino:ShowOpenDialog';
export const CHANNEL_SHOW_SAVE_DIALOG = 'Arduino:ShowSaveDialog';
export const CHANNEL_APP_VERSION = 'Arduino:AppVersion';
export const CHANNEL_QUIT_APP = 'Arduino:QuitApp';
export const CHANNEL_IS_FIRST_WINDOW = 'Arduino:IsFirstWindow';
export const CHANNEL_SCHEDULE_DELETION = 'Arduino:ScheduleDeletion';
export const CHANNEL_SET_REPRESENTED_FILENAME =
'Arduino:SetRepresentedFilename';
export const CHANNEL_SHOW_PLOTTER_WINDOW = 'Arduino:ShowPlotterWindow';
export const CHANNEL_OPEN_PATH = 'Arduino:OpenPath';
export const CHANNEL_SET_MENU_WITH_NODE_ID = 'Arduino:SetMenuWithNodeId';
// main to renderer
export const CHANNEL_SEND_STARTUP_TASKS = 'Arduino:SendStartupTasks';
export const CHANNEL_PLOTTER_WINDOW_DID_CLOSE = 'Arduino:PlotterWindowDidClose';
export const CHANNEL_MAIN_MENU_ITEM_DID_CLICK = 'Arduino:MainMenuItemDidClick';

View File

@ -1,7 +0,0 @@
export const electronMainWindowServiceExtPath = '/services/electron-window-ext';
export const ElectronMainWindowServiceExt = Symbol(
'ElectronMainWindowServiceExt'
);
export interface ElectronMainWindowServiceExt {
isFirstWindow(windowId: number): Promise<boolean>;
}

View File

@ -1 +0,0 @@
export const SCHEDULE_DELETION_SIGNAL = 'arduino/scheduleDeletion';

View File

@ -1,50 +1,46 @@
export const StartupTaskProvider = Symbol('StartupTaskProvider');
export interface StartupTaskProvider {
tasks(): StartupTask[];
}
export interface StartupTask { export interface StartupTask {
command: string; readonly command: string;
/** /**
* Must be JSON serializable. * Must be JSON serializable.
* See the restrictions [here](https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args). * See the restrictions [here](https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args).
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[]; readonly args?: any[];
}
export namespace StartupTask {
export function is(arg: unknown): arg is StartupTask {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'command' in object &&
typeof object['command'] === 'string' &&
(!('args' in object) || Array.isArray(object['args']))
);
}
return false;
}
export function has(arg: unknown): arg is unknown & Owner {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'tasks' in object &&
Array.isArray(object['tasks']) &&
object['tasks'].every(is)
);
}
return false;
}
export namespace Messaging {
export const STARTUP_TASKS_SIGNAL = 'arduino/startupTasks';
export function APP_READY_SIGNAL(id: number): string {
return `arduino/appReady${id}`;
}
} }
export interface Owner { export interface StartupTasks {
readonly tasks: StartupTask[]; readonly tasks: StartupTask[];
} }
export function isStartupTask(arg: unknown): arg is StartupTask {
if (typeof arg === 'object') {
if (
(<StartupTask>arg).command !== undefined &&
typeof (<StartupTask>arg).command === 'string'
) {
return (
(<StartupTask>arg).args === undefined ||
((<StartupTask>arg).args !== undefined &&
Array.isArray((<StartupTask>arg).args))
);
}
}
return false;
} }
export const StartupTaskProvider = Symbol('StartupTaskProvider'); export function hasStartupTasks(arg: unknown): arg is unknown & StartupTasks {
export interface StartupTaskProvider { if (typeof arg === 'object') {
tasks(): StartupTask[]; return (
(<StartupTasks>arg).tasks !== undefined &&
Array.isArray((<StartupTasks>arg).tasks) &&
Boolean((<StartupTasks>arg).tasks.length) &&
(<StartupTasks>arg).tasks.every(isStartupTask)
);
}
return false;
} }

View File

@ -5,7 +5,6 @@ import {
ElectronMainApplication as TheiaElectronMainApplication, ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution, ElectronMainApplicationContribution,
} from '@theia/core/lib/electron-main/electron-main-application'; } from '@theia/core/lib/electron-main/electron-main-application';
import { ElectronMessagingContribution as TheiaElectronMessagingContribution } from '@theia/core/lib/electron-main/messaging/electron-messaging-contribution';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { ContainerModule } from '@theia/core/shared/inversify'; import { ContainerModule } from '@theia/core/shared/inversify';
import { import {
@ -13,12 +12,11 @@ import {
IDEUpdaterClient, IDEUpdaterClient,
IDEUpdaterPath, IDEUpdaterPath,
} from '../common/protocol/ide-updater'; } from '../common/protocol/ide-updater';
import { electronMainWindowServiceExtPath } from '../electron-common/electron-main-window-service-ext';
import { IsTempSketch } from '../node/is-temp-sketch'; import { IsTempSketch } from '../node/is-temp-sketch';
import { ElectronArduino } from './electron-arduino';
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl'; import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { ElectronMainApplication } from './theia/electron-main-application'; import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service'; import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
import { ElectronMessagingContribution } from './theia/electron-messaging-contribution';
import { TheiaElectronWindow } from './theia/theia-electron-window'; import { TheiaElectronWindow } from './theia/theia-electron-window';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
@ -50,20 +48,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf(); bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow); rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () =>
context.container.get(ElectronMainWindowServiceImpl)
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope(); bind(IsTempSketch).toSelf().inSingletonScope();
// Fix for cannot reload window: https://github.com/eclipse-theia/theia/issues/11600 bind(ElectronArduino).toSelf().inSingletonScope();
bind(ElectronMessagingContribution).toSelf().inSingletonScope(); bind(ElectronMainApplicationContribution).toService(ElectronArduino);
rebind(TheiaElectronMessagingContribution).toService(
ElectronMessagingContribution
);
}); });

Some files were not shown because too many files have changed in this diff Show More