Compare commits

..

1 Commits

Author SHA1 Message Date
Francesco Spissu
f84c15bea0 close websocket connection after closing serial plotter 2022-11-21 11:53:56 +01:00
250 changed files with 8405 additions and 14182 deletions

View File

@@ -15,6 +15,7 @@ module.exports = {
'.browser_modules/*', '.browser_modules/*',
'docs/*', 'docs/*',
'scripts/*', 'scripts/*',
'electron/*',
'electron-app/*', 'electron-app/*',
'plugins/*', 'plugins/*',
'arduino-ide-extension/src/node/cli-protocol', 'arduino-ide-extension/src/node/cli-protocol',

View File

@@ -180,14 +180,10 @@ jobs:
fi fi
fi fi
echo -e "$BODY" echo -e "$BODY"
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
# Set workflow step output OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
DELIMITER="$RANDOM" echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "$DELIMITER" >> $GITHUB_OUTPUT
echo "$BODY" > CHANGELOG.txt echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions] - name: Upload Changelog [GitHub Actions]
@@ -235,7 +231,7 @@ jobs:
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub] - name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.4.1 uses: svenstaro/upload-release-action@2.3.0
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }} release_name: ${{ steps.tag_name.outputs.TAG_NAME }}

View File

@@ -59,9 +59,7 @@ jobs:
( (
openssl pkcs12 \ openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \ -in "${{ env.CERTIFICATE_PATH }}" \
-legacy \ -noout -passin env:CERTIFICATE_PASSWORD
-noout \
-passin env:CERTIFICATE_PASSWORD
) || ( ) || (
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!" echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
exit 1 exit 1
@@ -89,7 +87,6 @@ jobs:
openssl pkcs12 \ openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \ -in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \ -clcerts \
-legacy \
-nodes \ -nodes \
-passin env:CERTIFICATE_PASSWORD -passin env:CERTIFICATE_PASSWORD
) | ( ) | (

View File

@@ -48,8 +48,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for errors - name: Check for errors
run: yarn i18n:check run: yarn i18n:check

8
.gitignore vendored
View File

@@ -4,10 +4,10 @@ node_modules/
lib/ lib/
downloads/ downloads/
build/ build/
arduino-ide-extension/Examples/ Examples/
!electron/build/ !electron/build/
src-gen/ src-gen/
webpack.config.js !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.
@@ -15,11 +15,11 @@ gen-webpack.config.js
yarn*.log yarn*.log
# For the VS Code extensions used by Theia. # For the VS Code extensions used by Theia.
plugins plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes # the tokens folder for the themes
scripts/themes/tokens scripts/themes/tokens
# environment variables # environment variables
.env .env
# content trace files for electron # content trace files for electron
electron-app/traces electron-app/traces
# any Arduino LS generated log files
inols*.log

8
.vscode/launch.json vendored
View File

@@ -14,14 +14,14 @@
".", ".",
"--log-level=debug", "--log-level=debug",
"--hostname=localhost", "--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app", "--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222", "--remote-debugging-port=9222",
"--no-app-auto-install", "--no-app-auto-install",
"--plugins=local-dir:../plugins", "--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339", "--hosted-plugin-inspect=9339",
"--content-trace", "--content-trace",
"--open-devtools", "--open-devtools"
"--no-ping-timeout",
], ],
"env": { "env": {
"NODE_ENV": "development" "NODE_ENV": "development"
@@ -51,12 +51,12 @@
".", ".",
"--log-level=debug", "--log-level=debug",
"--hostname=localhost", "--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app", "--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222", "--remote-debugging-port=9222",
"--no-app-auto-install", "--no-app-auto-install",
"--plugins=local-dir:../plugins", "--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339", "--hosted-plugin-inspect=9339"
"--no-ping-timeout",
], ],
"env": { "env": {
"NODE_ENV": "development" "NODE_ENV": "development"

View File

@@ -2,9 +2,6 @@
"files.exclude": { "files.exclude": {
"**/lib": false "**/lib": false
}, },
"search.exclude": {
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
},
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true

View File

@@ -1,6 +1,6 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "2.0.4", "version": "2.0.3",
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
@@ -17,36 +17,31 @@
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint", "build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w", "watch": "tsc -w",
"test": "mocha \"./lib/test/**/*.test.js\"", "test": "mocha \"./lib/test/**/*.test.js\"",
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
"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.6.7",
"@theia/application-package": "1.31.1", "@theia/application-package": "1.25.0",
"@theia/core": "1.31.1", "@theia/core": "1.25.0",
"@theia/debug": "1.31.1", "@theia/editor": "1.25.0",
"@theia/editor": "1.31.1", "@theia/electron": "1.25.0",
"@theia/electron": "1.31.1", "@theia/filesystem": "1.25.0",
"@theia/filesystem": "1.31.1", "@theia/keymaps": "1.25.0",
"@theia/keymaps": "1.31.1", "@theia/markers": "1.25.0",
"@theia/markers": "1.31.1", "@theia/monaco": "1.25.0",
"@theia/messages": "1.31.1", "@theia/navigator": "1.25.0",
"@theia/monaco": "1.31.1", "@theia/outline-view": "1.25.0",
"@theia/monaco-editor-core": "1.67.2", "@theia/output": "1.25.0",
"@theia/navigator": "1.31.1", "@theia/preferences": "1.25.0",
"@theia/outline-view": "1.31.1", "@theia/search-in-workspace": "1.25.0",
"@theia/output": "1.31.1", "@theia/terminal": "1.25.0",
"@theia/plugin-ext": "1.31.1", "@theia/workspace": "1.25.0",
"@theia/preferences": "1.31.1",
"@theia/scm": "1.31.1",
"@theia/search-in-workspace": "1.31.1",
"@theia/terminal": "1.31.1",
"@theia/typehierarchy": "1.31.1",
"@theia/workspace": "1.31.1",
"@tippyjs/react": "^4.2.5", "@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0", "@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3", "@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1", "@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0", "@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0", "@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2", "@types/google-protobuf": "^3.7.2",
@@ -55,53 +50,49 @@
"@types/lodash.debounce": "^4.0.6", "@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4", "@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0", "@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2", "@types/react-tabs": "^2.3.2",
"@types/react-virtualized": "^9.21.21",
"@types/temp": "^0.8.34", "@types/temp": "^0.8.34",
"@types/which": "^1.3.1", "@types/which": "^1.3.1",
"@vscode/debugprotocol": "^1.51.0", "ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.2.0", "arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0", "async-mutex": "^0.3.0",
"atob": "^2.1.2",
"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",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1", "deepmerge": "2.0.1",
"electron-updater": "^4.6.5", "electron-updater": "^4.6.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1", "fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
"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-valid-path": "^0.1.1",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"keytar": "7.2.0", "keytar": "7.2.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"open": "^8.0.6", "open": "^8.0.6",
"p-debounce": "^2.1.0", "p-queue": "^5.0.0",
"p-queue": "^2.4.2",
"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.0",
"react-markdown": "^8.0.0", "react-markdown": "^8.0.0",
"react-perfect-scrollbar": "^1.5.8", "react-select": "^3.0.4",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2", "react-tabs": "^3.1.2",
"react-virtualized": "^9.22.3",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"semver": "^7.3.2", "semver": "^7.3.2",
"string-natural-compare": "^2.0.3", "string-natural-compare": "^2.0.3",
"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",
"upath": "^1.1.2",
"url": "^0.11.0",
"which": "^1.3.1" "which": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
@@ -110,10 +101,11 @@
"@types/chai-string": "^1.4.2", "@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-string": "^1.5.0", "chai-string": "^1.5.0",
"decompress": "^4.2.0", "decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1", "decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1", "decompress-unzip": "^4.0.1",
"download": "^7.1.0", "download": "^7.1.0",
@@ -123,6 +115,9 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"protoc": "^1.0.4", "protoc": "^1.0.4",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"yargs": "^11.1.0" "yargs": "^11.1.0"
}, },
@@ -172,7 +167,7 @@
"version": "14.0.0" "version": "14.0.0"
}, },
"languageServer": { "languageServer": {
"version": "0.7.4" "version": "0.7.2"
} }
} }
} }

View File

@@ -31,7 +31,7 @@ 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 { ArduinoPreferences } from './arduino-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';
@@ -58,8 +58,8 @@ export class ArduinoFrontendContribution
@inject(CommandRegistry) @inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry; private readonly commandRegistry: CommandRegistry;
@inject(ElectronWindowPreferences) @inject(ArduinoPreferences)
private readonly electronWindowPreferences: ElectronWindowPreferences; private readonly arduinoPreferences: ArduinoPreferences;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService; private readonly appStateService: FrontendApplicationStateService;
@@ -78,10 +78,10 @@ export class ArduinoFrontendContribution
} }
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
this.electronWindowPreferences.onPreferenceChanged((event) => { this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) { if (event.newValue !== event.oldValue) {
switch (event.preferenceName) { switch (event.preferenceName) {
case 'window.zoomLevel': case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') { if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents(); const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0); webContents.setZoomLevel(event.newValue || 0);
@@ -91,10 +91,11 @@ export class ArduinoFrontendContribution
} }
}); });
this.appStateService.reachedState('ready').then(() => this.appStateService.reachedState('ready').then(() =>
this.electronWindowPreferences.ready.then(() => { this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents(); const webContents = remote.getCurrentWebContents();
const zoomLevel = const zoomLevel = this.arduinoPreferences.get(
this.electronWindowPreferences.get('window.zoomLevel'); 'arduino.window.zoomLevel'
);
webContents.setZoomLevel(zoomLevel); webContents.setZoomLevel(zoomLevel);
}) })
); );

View File

@@ -1,9 +1,12 @@
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';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import {
TabBarToolbarContribution,
TabBarToolbarFactory,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
@@ -23,7 +26,7 @@ import {
SketchesService, SketchesService,
SketchesServicePath, SketchesServicePath,
} from '../common/protocol/sketches-service'; } from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from './sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget'; import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -81,7 +84,10 @@ import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer'; import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import {
MonacoThemeJson,
MonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import { import {
ArduinoDaemonPath, ArduinoDaemonPath,
ArduinoDaemon, ArduinoDaemon,
@@ -127,10 +133,11 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
import { PreferencesContribution } from './theia/preferences/preferences-contribution'; import { PreferencesContribution } from './theia/preferences/preferences-contribution';
import { QuitApp } from './contributions/quit-app'; import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control'; import { SketchControl } from './contributions/sketch-control';
import { OpenSettings } from './contributions/open-settings'; import { Settings } from './contributions/settings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands'; import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler'; import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler'; import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory'; import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory'; import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { BurnBootloader } from './contributions/burn-bootloader'; import { BurnBootloader } from './contributions/burn-bootloader';
@@ -174,6 +181,8 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator'; import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator'; import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug } from './contributions/debug'; import { Debug } from './contributions/debug';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook'; import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution'; import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution'; import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
@@ -232,6 +241,7 @@ import { UploadFirmware } from './contributions/upload-firmware';
import { import {
UploadFirmwareDialog, UploadFirmwareDialog,
UploadFirmwareDialogProps, UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog'; } from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate'; import { UploadCertificate } from './contributions/upload-certificate';
@@ -248,6 +258,7 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c
import { import {
UserFieldsDialog, UserFieldsDialog,
UserFieldsDialogProps, UserFieldsDialogProps,
UserFieldsDialogWidget,
} from './dialogs/user-fields/user-fields-dialog'; } from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands'; import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
@@ -260,6 +271,7 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
import { import {
IDEUpdaterDialog, IDEUpdaterDialog,
IDEUpdaterDialogProps, IDEUpdaterDialogProps,
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog'; } from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model'; import { MonitorModel } from './monitor-model';
@@ -301,6 +313,10 @@ import { SelectedBoard } from './contributions/selected-board';
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates'; import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
import { OpenBoardsConfig } from './contributions/open-boards-config'; import { OpenBoardsConfig } from './contributions/open-boards-config';
import { SketchFilesTracker } from './contributions/sketch-files-tracker'; import { SketchFilesTracker } from './contributions/sketch-files-tracker';
import { MonacoThemeServiceIsReady } from './utils/window';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { StatusBarImpl } from './theia/core/status-bar';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
import { EditorMenuContribution } from './theia/editor/editor-file'; import { EditorMenuContribution } from './theia/editor/editor-file';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu'; import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget'; import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
@@ -321,32 +337,32 @@ import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service'; import { OpenHandler } from '@theia/core/lib/browser/opener-service';
import { NewCloudSketch } from './contributions/new-cloud-sketch'; import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget'; import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater'; const registerArduinoThemes = () => {
import { ThemeServiceWithDB } from './theia/core/theming'; const themes: MonacoThemeJson[] = [
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db'; {
import { MonacoThemingService } from './theia/monaco/monaco-theming-service'; id: 'arduino-theme',
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; label: 'Light (Arduino)',
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service'; uiTheme: 'vs',
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service'; json: require('../../src/browser/data/default.color-theme.json'),
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution'; },
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution'; {
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution'; id: 'arduino-theme-dark',
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution'; label: 'Dark (Arduino)',
import { DebugToolbar } from './theia/debug/debug-toolbar-widget'; uiTheme: 'vs-dark',
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; json: require('../../src/browser/data/dark.color-theme.json'),
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'; themes.forEach((theme) => MonacoThemingService.register(theme));
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; };
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget'; // eslint-disable-next-line @typescript-eslint/no-explicit-any
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model'; const global = window as any;
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget'; const ready = global[MonacoThemeServiceIsReady] as Deferred;
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget'; if (ready) {
import { ConfigServiceClient } from './config/config-service-client'; ready.promise.then(registerArduinoThemes);
import { ValidateSketch } from './contributions/validate-sketch'; } else {
import { RenameCloudSketch } from './contributions/rename-cloud-sketch'; registerArduinoThemes();
import { CreateFeatures } from './create/create-features'; }
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items // Commands and toolbar items
@@ -408,8 +424,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
) )
) )
.inSingletonScope(); .inSingletonScope();
bind(ConfigServiceClient).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
// Boards service // Boards service
bind(BoardsService) bind(BoardsService)
@@ -573,6 +587,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(WorkspaceDeleteHandler) .to(WorkspaceDeleteHandler)
.inSingletonScope(); .inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope(); rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputChannelManager).toSelf().inSingletonScope(); bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager); rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope(); bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -697,7 +719,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, EditContributions); Contribution.configure(bind, EditContributions);
Contribution.configure(bind, QuitApp); Contribution.configure(bind, QuitApp);
Contribution.configure(bind, SketchControl); Contribution.configure(bind, SketchControl);
Contribution.configure(bind, OpenSettings); Contribution.configure(bind, Settings);
Contribution.configure(bind, BurnBootloader); Contribution.configure(bind, BurnBootloader);
Contribution.configure(bind, BuiltInExamples); Contribution.configure(bind, BuiltInExamples);
Contribution.configure(bind, LibraryExamples); Contribution.configure(bind, LibraryExamples);
@@ -732,8 +754,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, UpdateIndexes); Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale); Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch); Contribution.configure(bind, NewCloudSketch);
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
bindContributionProvider(bind, StartupTaskProvider); bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -818,6 +838,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(AboutDialog).toSelf().inSingletonScope(); bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog); rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu. // To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope(); bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService( rebind(TheiaDebugFrontendApplicationContribution).toService(
@@ -831,6 +854,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(WidgetManager).toSelf().inSingletonScope(); bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager); rebind(TheiaWidgetManager).toService(WidgetManager);
// To avoid running a status bar update on every single `keypress` event from the editor.
bind(StatusBarImpl).toSelf().inSingletonScope();
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
// Debounced update for the tab-bar toolbar when typing in the editor. // Debounced update for the tab-bar toolbar when typing in the editor.
bind(DockPanelRenderer).toSelf(); bind(DockPanelRenderer).toSelf();
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer); rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
@@ -894,8 +921,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
); );
bind(CreateApi).toSelf().inSingletonScope(); bind(CreateApi).toSelf().inSingletonScope();
bind(SketchCache).toSelf().inSingletonScope(); bind(SketchCache).toSelf().inSingletonScope();
bind(CreateFeatures).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFeatures);
bind(ShareSketchDialog).toSelf().inSingletonScope(); bind(ShareSketchDialog).toSelf().inSingletonScope();
bind(AuthenticationClientService).toSelf().inSingletonScope(); bind(AuthenticationClientService).toSelf().inSingletonScope();
@@ -917,11 +942,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(LocalCacheFsProvider).toSelf().inSingletonScope(); bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider); bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf(); bind(CloudSketchbookCompositeWidget).toSelf();
bind(WidgetFactory).toDynamicValue((ctx) => ({ bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'cloud-sketchbook-composite-widget', id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget), createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
})); }));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope(); bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({ bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware', title: 'UploadFirmware',
@@ -932,11 +958,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'UploadCertificate', title: 'UploadCertificate',
}); });
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
bind(IDEUpdaterDialog).toSelf().inSingletonScope(); bind(IDEUpdaterDialog).toSelf().inSingletonScope();
bind(IDEUpdaterDialogProps).toConstantValue({ bind(IDEUpdaterDialogProps).toConstantValue({
title: 'IDEUpdater', title: 'IDEUpdater',
}); });
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({ bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields', title: 'UserFields',
@@ -963,55 +991,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport); rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope(); bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents); bind(FrontendApplicationContribution).toService(HostedPluginEvents);
// custom window titles
bind(WindowTitleUpdater).toSelf().inSingletonScope();
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
bind(ThemeServiceWithDB).toSelf().inSingletonScope();
rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyServiceProvider).toService(
TypeHierarchyServiceProvider
);
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
// patched the debugger for `cortex-debug@1.5.1`
// https://github.com/eclipse-theia/theia/issues/11871
// https://github.com/eclipse-theia/theia/issues/11879
// https://github.com/eclipse-theia/theia/issues/11880
// https://github.com/eclipse-theia/theia/issues/11885
// https://github.com/eclipse-theia/theia/issues/11886
// https://github.com/eclipse-theia/theia/issues/11916
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
bind(DebugToolbar).toSelf().inSingletonScope();
rebind(TheiaDebugToolbar).toService(DebugToolbar);
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: DebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolbar).toSelf(); // patched toolbar
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf();
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();
}); });

View File

@@ -114,12 +114,11 @@ export const ArduinoConfigSchema: PreferenceSchema = {
}, },
'arduino.window.zoomLevel': { 'arduino.window.zoomLevel': {
type: 'number', type: 'number',
description: '', description: nls.localize(
default: 0, 'arduino/preferences/window.zoomLevel',
deprecationMessage: nls.localize( 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
), ),
default: 0,
}, },
'arduino.ide.updateChannel': { 'arduino.ide.updateChannel': {
type: 'string', type: 'string',
@@ -271,6 +270,7 @@ export interface ArduinoConfiguration {
'arduino.upload.verbose': boolean; 'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean; 'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean; 'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.updateChannel': UpdateChannel; 'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string; 'arduino.ide.updateBaseUrl': string;
'arduino.board.certificates': string; 'arduino.board.certificates': string;

View File

@@ -0,0 +1,68 @@
import { URI } from '@theia/core/shared/vscode-uri';
import { isWindows } from '@theia/core/lib/common/os';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
/**
* Class for determining the default workspace location from the
* `location.hash`, the historical workspace locations, and recent sketch files.
*
* The following logic is used for determining the default workspace location:
* - `hash` points to an existing location?
* - Yes
* - `validate location`. Is valid sketch location?
* - Yes
* - Done.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
*/
namespace ArduinoWorkspaceRootResolver {
export interface InitOptions {
readonly isValid: (uri: string) => MaybePromise<boolean>;
}
export interface ResolveOptions {
readonly hash?: string;
readonly recentWorkspaces: string[];
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
readonly recentSketches: string[];
}
}
export class ArduinoWorkspaceRootResolver {
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
async resolve(
options: ArduinoWorkspaceRootResolver.ResolveOptions
): Promise<{ uri: string } | undefined> {
const { hash, recentWorkspaces, recentSketches } = options;
for (const uri of [
this.hashToUri(hash),
...recentWorkspaces,
...recentSketches,
].filter(notEmpty)) {
const valid = await this.isValid(uri);
if (valid) {
return { uri };
}
}
return undefined;
}
protected isValid(uri: string): MaybePromise<boolean> {
return this.options.isValid(uri);
}
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on POSIX.
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
protected hashToUri(hash: string | undefined): string | undefined {
if (hash && hash.length > 1 && hash.startsWith('#')) {
const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators
return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString();
}
return undefined;
}
}

View File

@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { import {
Board, Board,
Port, Port,
BoardConfig as ProtocolBoardConfig,
BoardWithPackage, BoardWithPackage,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
@@ -19,7 +18,10 @@ import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state'; import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig { export namespace BoardsConfig {
export type Config = ProtocolBoardConfig; export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export interface Props { export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider; readonly boardsServiceProvider: BoardsServiceProvider;

View File

@@ -80,16 +80,16 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
string, string,
Disposable & { label: string } Disposable & { label: string }
>(); >();
let selectedValue = '';
for (const value of values) { for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`; const id = `${fqbn}-${option}--${value.value}`;
const command = { id }; const command = { id };
const selectedValue = value.value;
const handler = { const handler = {
execute: () => execute: () =>
this.boardsDataStore.selectConfigOption({ this.boardsDataStore.selectConfigOption({
fqbn, fqbn,
option, option,
selectedValue: value.value, selectedValue,
}), }),
isToggled: () => value.selected, isToggled: () => value.selected,
}; };
@@ -100,14 +100,8 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
{ label: value.label } { label: value.label }
) )
); );
if (value.selected) {
selectedValue = value.label;
} }
} this.menuRegistry.registerSubmenu(menuPath, label);
this.menuRegistry.registerSubmenu(
menuPath,
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
);
this.toDisposeOnBoardChange.pushAll([ this.toDisposeOnBoardChange.pushAll([
...commands.values(), ...commands.values(),
Disposable.create(() => Disposable.create(() =>

View File

@@ -30,11 +30,11 @@ export class BoardsDataStore implements FrontendApplicationContribution {
@inject(LocalStorageService) @inject(LocalStorageService)
protected readonly storageService: LocalStorageService; protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<string[]>(); protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void { onStart(): void {
this.notificationCenter.onPlatformDidInstall(async ({ item }) => { this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
const dataDidChangePerFqbn: string[] = []; let shouldFireChanged = false;
for (const fqbn of item.boards for (const fqbn of item.boards
.map(({ fqbn }) => fqbn) .map(({ fqbn }) => fqbn)
.filter(notEmpty) .filter(notEmpty)
@@ -49,18 +49,18 @@ export class BoardsDataStore implements FrontendApplicationContribution {
data = details.configOptions; data = details.configOptions;
if (data.length) { if (data.length) {
await this.storageService.setData(key, data); await this.storageService.setData(key, data);
dataDidChangePerFqbn.push(fqbn); shouldFireChanged = true;
} }
} }
} }
} }
if (dataDidChangePerFqbn.length) { if (shouldFireChanged) {
this.fireChanged(...dataDidChangePerFqbn); this.fireChanged();
} }
}); });
} }
get onChanged(): Event<string[]> { get onChanged(): Event<void> {
return this.onChangedEmitter.event; return this.onChangedEmitter.event;
} }
@@ -116,7 +116,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn, fqbn,
data: { ...data, selectedProgrammer }, data: { ...data, selectedProgrammer },
}); });
this.fireChanged(fqbn); this.fireChanged();
return true; return true;
} }
@@ -146,7 +146,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
return false; return false;
} }
await this.setData({ fqbn, data }); await this.setData({ fqbn, data });
this.fireChanged(fqbn); this.fireChanged();
return true; return true;
} }
@@ -190,8 +190,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
} }
} }
protected fireChanged(...fqbn: string[]): void { protected fireChanged(): void {
this.onChangedEmitter.fire(fqbn); this.onChangedEmitter.fire();
} }
} }

View File

@@ -30,6 +30,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: BoardsPackage) => item.name, itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer, itemRenderer,
filterRenderer, filterRenderer,
defaultSearchOptions: { query: '', type: 'All' }, defaultSearchOptions: { query: '', type: 'All' },

View File

@@ -1,102 +0,0 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { deepClone } from '@theia/core/lib/common/objects';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { ConfigService, ConfigState } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
@injectable()
export class ConfigServiceClient implements FrontendApplicationContribution {
@inject(ConfigService)
private readonly delegate: ConfigService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(MessageService)
private readonly messageService: MessageService;
private readonly didChangeSketchDirUriEmitter = new Emitter<
URI | undefined
>();
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
private readonly toDispose = new DisposableCollection(
this.didChangeSketchDirUriEmitter,
this.didChangeDataDirUriEmitter
);
private config: ConfigState | undefined;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(async () => {
const config = await this.delegate.getConfiguration();
this.use(config);
});
}
onStart(): void {
this.notificationCenter.onConfigDidChange((config) => this.use(config));
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSketchDirUri(): Event<URI | undefined> {
return this.didChangeSketchDirUriEmitter.event;
}
get onDidChangeDataDirUri(): Event<URI | undefined> {
return this.didChangeDataDirUriEmitter.event;
}
/**
* CLI config related error messages if any.
*/
tryGetMessages(): string[] | undefined {
return this.config?.messages;
}
/**
* `directories.user`
*/
tryGetSketchDirUri(): URI | undefined {
return this.config?.config?.sketchDirUri
? new URI(this.config?.config?.sketchDirUri)
: undefined;
}
/**
* `directories.data`
*/
tryGetDataDirUri(): URI | undefined {
return this.config?.config?.dataDirUri
? new URI(this.config?.config?.dataDirUri)
: undefined;
}
private use(config: ConfigState): void {
const oldConfig = deepClone(this.config);
this.config = config;
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
}
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
}
if (this.config.messages?.length) {
const message = this.config.messages.join(' ');
// toast the error later otherwise it might not show up in IDE2
setTimeout(() => this.messageService.error(message), 1_000);
}
}
}

View File

@@ -41,16 +41,22 @@ export class About extends Contribution {
} }
async showAbout(): Promise<void> { async showAbout(): Promise<void> {
const version = await this.configService.getVersion(); const {
version,
commit,
status: cliStatus,
} = await 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}{4} [{5}]\n\n{6}',
remote.app.getVersion(), remote.app.getVersion(),
buildDate ? buildDate : nls.localize('', 'dev build'), buildDate ? buildDate : nls.localize('', 'dev build'),
buildDate && showAll ? ` (${this.ago(buildDate)})` : '', buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
version, version,
cliStatus ? ` ${cliStatus}` : '',
commit,
nls.localize( nls.localize(
'arduino/about/copyright', 'arduino/about/copyright',
'Copyright © {0} Arduino SA', 'Copyright © {0} Arduino SA',

View File

@@ -7,11 +7,10 @@ import {
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI, URI,
Sketch,
} from './contribution'; } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser'; import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable() @injectable()
export class AddFile extends SketchContribution { export class AddFile extends SketchContribution {
@@ -47,7 +46,9 @@ export class AddFile extends SketchContribution {
if (!toAddUri) { if (!toAddUri) {
return; return;
} }
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri); const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
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 remote.dialog.showMessageBox({
@@ -79,22 +80,6 @@ export class AddFile extends SketchContribution {
} }
); );
} }
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
private resolveTarget(
sketch: Sketch,
toAddUri: URI
): { uri: URI; filename: string } {
const path = toAddUri.path;
const filename = path.base;
let root = new URI(sketch.uri);
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
root = root.resolve('data');
}
return { uri: root.resolve(filename), filename: filename };
}
} }
export namespace AddFile { export namespace AddFile {

View File

@@ -2,6 +2,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote'; 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 { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryService, ResponseServiceClient } from '../../common/protocol'; import { LibraryService, ResponseServiceClient } from '../../common/protocol';
import { ExecuteWithProgress } from '../../common/protocol/progressible'; import { ExecuteWithProgress } from '../../common/protocol/progressible';
@@ -15,6 +16,9 @@ import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class AddZipLibrary extends SketchContribution { export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
private readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient) @inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient; private readonly responseService: ResponseServiceClient;

View File

@@ -1,6 +1,7 @@
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 * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat'; import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
@@ -9,7 +10,7 @@ import {
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable() @injectable()
export class ArchiveSketch extends SketchContribution { export class ArchiveSketch extends SketchContribution {
@@ -28,7 +29,10 @@ export class ArchiveSketch extends SketchContribution {
} }
private async archiveSketch(): Promise<void> { private async archiveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch(); const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return; return;
} }
@@ -36,9 +40,9 @@ export class ArchiveSketch extends SketchContribution {
new Date(), new Date(),
'yymmdd' 'yymmdd'
)}a.zip`; )}a.zip`;
const defaultContainerUri = await this.defaultUri(); const defaultPath = await this.fileService.fsPath(
const defaultUri = defaultContainerUri.resolve(archiveBasename); new URI(config.sketchDirUri).resolve(archiveBasename)
const defaultPath = await this.fileService.fsPath(defaultUri); );
const { filePath, canceled } = await remote.dialog.showSaveDialog( const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
@@ -56,7 +60,7 @@ export class ArchiveSketch extends SketchContribution {
if (!destinationUri) { if (!destinationUri) {
return; return;
} }
await this.sketchesService.archive(sketch, destinationUri.toString()); await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info( this.messageService.info(
nls.localize( nls.localize(
'arduino/sketch/createdArchive', 'arduino/sketch/createdArchive',

View File

@@ -20,7 +20,6 @@ import {
InstalledBoardWithPackage, InstalledBoardWithPackage,
AvailablePorts, AvailablePorts,
Port, Port,
getBoardInfo,
} from '../../common/protocol'; } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution'; import { SketchContribution, Command, CommandRegistry } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
@@ -50,21 +49,44 @@ export class BoardSelection extends SketchContribution {
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, { registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => { execute: async () => {
const boardInfo = await getBoardInfo( const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig.selectedPort, this.boardsServiceProvider.boardsConfig;
this.boardsService.getState() if (!selectedBoard) {
this.messageService.info(
nls.localize(
'arduino/board/selectBoardForInfo',
'Please select a board to obtain board info.'
)
); );
if (typeof boardInfo === 'string') {
this.messageService.info(boardInfo);
return; return;
} }
const { BN, VID, PID, SN } = boardInfo; if (!selectedBoard.fqbn) {
const detail = ` this.messageService.info(
BN: ${BN} nls.localize(
'arduino/board/platformMissing',
"The platform for the selected '{0}' board is not installed.",
selectedBoard.name
)
);
return;
}
if (!selectedPort) {
this.messageService.info(
nls.localize(
'arduino/board/selectPortForInfo',
'Please select a port to obtain board info.'
)
);
return;
}
const boardDetails = await this.boardsService.getBoardDetails({
fqbn: selectedBoard.fqbn,
});
if (boardDetails) {
const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name}
VID: ${VID} VID: ${VID}
PID: ${PID} PID: ${PID}`;
SN: ${SN}
`.trim();
await remote.dialog.showMessageBox(remote.getCurrentWindow(), { await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
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'),
@@ -72,6 +94,7 @@ SN: ${SN}
detail, detail,
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')], buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
}); });
}
}, },
}); });
} }
@@ -132,7 +155,10 @@ SN: ${SN}
); );
// Ports submenu // Ports submenu
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU; const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address; const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu( this.menuModelRegistry.registerSubmenu(
portsSubmenuPath, portsSubmenuPath,

View File

@@ -20,7 +20,7 @@ import {
URI, URI,
} from './contribution'; } from './contribution';
import { Dialog } from '@theia/core/lib/browser/dialogs'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch'; import { SaveAsSketch } from './save-as-sketch';
/** /**
@@ -185,7 +185,7 @@ export class Close extends SketchContribution {
private async isCurrentSketchTemp(): Promise<false | Sketch> { private async isCurrentSketchTemp(): Promise<false | Sketch> {
const currentSketch = await this.sketchServiceClient.currentSketch(); const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) { if (CurrentSketch.isValid(currentSketch)) {
const isTemp = await this.sketchesService.isTemp(currentSketch); const isTemp = await this.sketchService.isTemp(currentSketch);
if (isTemp) { if (isTemp) {
return currentSketch; return currentSketch;
} }

View File

@@ -1,121 +0,0 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CreateApi } from '../create/create-api';
import { CreateFeatures } from '../create/create-features';
import { CreateUri } from '../create/create-uri';
import { Create, isNotFound } from '../create/typings';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { SketchContribution } from './contribution';
export function sketchAlreadyExists(input: string): string {
return nls.localize(
'arduino/cloudSketch/alreadyExists',
"Cloud sketch '{0}' already exists.",
input
);
}
export function sketchNotFound(input: string): string {
return nls.localize(
'arduino/cloudSketch/notFound',
"Could not pull the cloud sketch '{0}'. It does not exist.",
input
);
}
export const synchronizingSketchbook = nls.localize(
'arduino/cloudSketch/synchronizingSketchbook',
'Synchronizing sketchbook...'
);
export function pullingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pulling',
"Synchronizing sketchbook, pulling '{0}'...",
input
);
}
export function pushingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pushing',
"Synchronizing sketchbook, pushing '{0}'...",
input
);
}
@injectable()
export abstract class CloudSketchContribution extends SketchContribution {
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(CreateFeatures)
protected readonly createFeatures: CreateFeatures;
protected async treeModel(): Promise<
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
> {
const { enabled, session } = this.createFeatures;
if (enabled && session) {
const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget);
if (treeModel) {
const root = treeModel.root;
if (CompositeTreeNode.is(root)) {
return treeModel as CloudSketchbookTreeModel & {
root: CompositeTreeNode;
};
}
}
}
return undefined;
}
protected async pull(
sketch: Create.Sketch
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
const treeModel = await this.treeModel();
if (!treeModel) {
return undefined;
}
const id = CreateUri.toUri(sketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find cloud sketchbook tree node with ID: ${id}.`
);
}
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node });
return node;
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(sketchNotFound(sketch.name));
return undefined;
}
throw err;
}
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
for (const treeWidget of widget.getTreeWidgets()) {
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
}
return undefined;
}
}

View File

@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { import {
@@ -41,9 +40,10 @@ import { SettingsService } from '../dialogs/settings/settings';
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../sketches-service-client-impl'; } from '../../common/protocol/sketches-service-client-impl';
import { import {
SketchesService, SketchesService,
ConfigService,
FileSystemExt, FileSystemExt,
Sketch, Sketch,
CoreService, CoreService,
@@ -61,8 +61,6 @@ import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager'; import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service'; import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
export { export {
Command, Command,
@@ -108,9 +106,6 @@ export abstract class Contribution
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService; protected readonly appStateService: FrontendApplicationStateService;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady()); this.appStateService.reachedState('ready').then(() => this.onReady());
@@ -143,11 +138,11 @@ export abstract class SketchContribution extends Contribution {
@inject(FileSystemExt) @inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt; protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigServiceClient) @inject(ConfigService)
protected readonly configService: ConfigServiceClient; protected readonly configService: ConfigService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchService: SketchesService;
@inject(OpenerService) @inject(OpenerService)
protected readonly openerService: OpenerService; protected readonly openerService: OpenerService;
@@ -161,9 +156,6 @@ export abstract class SketchContribution extends Contribution {
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager; protected readonly outputChannelManager: OutputChannelManager;
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
protected async sourceOverride(): Promise<Record<string, string>> { protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {}; const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
@@ -177,25 +169,6 @@ export abstract class SketchContribution extends Contribution {
} }
return override; return override;
} }
/**
* Defaults to `directories.user` if defined and not CLI config errors were detected.
* Otherwise, the URI of the user home directory.
*/
protected async defaultUri(): Promise<URI> {
const errors = this.configService.tryGetMessages();
let defaultUri = this.configService.tryGetSketchDirUri();
if (!defaultUri || errors?.length) {
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
}
return defaultUri;
}
protected async defaultPath(): Promise<string> {
const defaultUri = await this.defaultUri();
return this.fileService.fsPath(defaultUri);
}
} }
@injectable() @injectable()

View File

@@ -3,12 +3,7 @@ import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { import { Board, BoardsService, ExecutableService } from '../../common/protocol';
Board,
BoardsService,
ExecutableService,
Sketch,
} from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
URI, URI,
@@ -18,11 +13,12 @@ import {
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common'; import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; import { MainMenuManager } from '../../common/main-menu-manager';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
@injectable() @injectable()
export class Debug extends SketchContribution { export class Debug extends SketchContribution {
@inject(HostedPluginSupport) @inject(HostedPluginSupport)
@@ -40,6 +36,9 @@ export class Debug extends SketchContribution {
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider; private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
/** /**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled. * If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/ */
@@ -187,7 +186,7 @@ export class Debug extends SketchContribution {
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return; return;
} }
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri( const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
sketch sketch
); );
const [cliPath, sketchPath, configPath] = await Promise.all([ const [cliPath, sketchPath, configPath] = await Promise.all([
@@ -204,28 +203,7 @@ export class Debug extends SketchContribution {
sketchPath, sketchPath,
configPath, configPath,
}; };
try { return this.commandService.executeCommand('arduino.debug.start', config);
await this.commandService.executeCommand('arduino.debug.start', config);
} catch (err) {
if (await this.isSketchNotVerifiedError(err, sketch)) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const answer = await this.messageService.error(
nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketch.name
),
yes
);
if (answer === yes) {
this.commandService.executeCommand('arduino-verify-sketch');
}
} else {
this.messageService.error(
err instanceof Error ? err.message : String(err)
);
}
}
} }
get compileForDebug(): boolean { get compileForDebug(): boolean {
@@ -237,24 +215,7 @@ export class Debug extends SketchContribution {
const oldState = this.compileForDebug; const oldState = this.compileForDebug;
const newState = !oldState; const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState)); window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
this.menuManager.update(); this.mainMenuManager.update();
}
private async isSketchNotVerifiedError(
err: unknown,
sketch: Sketch
): Promise<boolean> {
if (err instanceof Error) {
try {
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return tempBuildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
return false;
}
}
return false;
} }
} }
export namespace Debug { export namespace Debug {

View File

@@ -1,131 +1,32 @@
import * as remote from '@theia/core/electron-shared/@electron/remote'; import { injectable } from '@theia/core/shared/inversify';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybeArray } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
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 {
import { Sketch } from '../contributions/contribution'; Command,
import { isNotFound } from '../create/typings'; CommandRegistry,
import { Command, CommandRegistry } from './contribution'; SketchContribution,
import { CloudSketchContribution } from './cloud-contribution'; Sketch,
} from './contribution';
export interface DeleteSketchParams {
/**
* Either the URI of the sketch folder or the sketch to delete.
*/
readonly toDelete: string | Sketch;
/**
* If `true`, the currently opened sketch is expected to be deleted.
* Hence, the editors must be closed, the sketch will be scheduled
* for deletion, and the browser window will close or navigate away.
* If `false`, the sketch will be scheduled for deletion,
* but the current window remains open. If `force`, the window will
* navigate away, but IDE2 won't open any confirmation dialogs.
*/
readonly willNavigateAway?: boolean | 'force';
}
@injectable() @injectable()
export class DeleteSketch extends CloudSketchContribution { export class DeleteSketch extends SketchContribution {
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, { registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (params: DeleteSketchParams) => this.deleteSketch(params), execute: (uri: string) => this.deleteSketch(uri),
}); });
} }
private async deleteSketch(params: DeleteSketchParams): Promise<void> { private async deleteSketch(uri: string): Promise<void> {
const { toDelete, willNavigateAway } = params; const sketch = await this.loadSketch(uri);
let sketch: Sketch; if (!sketch) {
if (typeof toDelete === 'string') { console.info(`Sketch not found at ${uri}. Skipping deletion.`);
const resolvedSketch = await this.loadSketch(toDelete);
if (!resolvedSketch) {
console.info(
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
);
return; return;
} }
sketch = resolvedSketch; return this.sketchService.deleteSketch(sketch);
} else {
sketch = toDelete;
}
if (!willNavigateAway) {
this.scheduleDeletion(sketch);
return;
}
const cloudUri = this.createFeatures.cloudUri(sketch);
if (willNavigateAway !== 'force') {
const { response } = await remote.dialog.showMessageBox({
title: nls.localizeByDefault('Delete'),
type: 'question',
buttons: [Dialog.CANCEL, Dialog.OK],
message: cloudUri
? nls.localize(
'theia/workspace/deleteCloudSketch',
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
)
: nls.localize(
'theia/workspace/deleteCurrentSketch',
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
),
});
// cancel
if (response === 0) {
return;
}
}
if (cloudUri) {
const posixPath = cloudUri.path.toString();
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
if (!cloudSketch) {
throw new Error(
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
);
}
try {
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
await this.createApi.deleteSketch(cloudSketch.path);
} catch (err) {
if (!isNotFound(err)) {
throw err;
} else {
console.info(
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
);
}
}
}
await Promise.all([
...Sketch.uris(sketch).map((uri) =>
this.closeWithoutSaving(new URI(uri))
),
]);
this.windowService.setSafeToShutDown();
this.scheduleDeletion(sketch);
return window.close();
}
private scheduleDeletion(sketch: Sketch): void {
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
} }
private async loadSketch(uri: string): Promise<Sketch | undefined> { private async loadSketch(uri: string): Promise<Sketch | undefined> {
try { try {
const sketch = await this.sketchesService.loadSketch(uri); const sketch = await this.sketchService.loadSketch(uri);
return sketch; return sketch;
} catch (err) { } catch (err) {
if (SketchesError.NotFound.is(err)) { if (SketchesError.NotFound.is(err)) {
@@ -134,13 +35,6 @@ export class DeleteSketch extends CloudSketchContribution {
throw err; throw err;
} }
} }
// fix: https://github.com/eclipse-theia/theia/issues/12107
private async closeWithoutSaving(uri: URI): Promise<void> {
const affected = getAffected(this.shell.widgets, uri);
const toClose = [...affected].map(([, widget]) => widget);
await this.shell.closeMany(toClose, { save: false });
}
} }
export namespace DeleteSketch { export namespace DeleteSketch {
export namespace Commands { export namespace Commands {
@@ -149,20 +43,3 @@ export namespace DeleteSketch {
}; };
} }
} }
function getAffected<T extends Widget>(
widgets: Iterable<T>,
context: MaybeArray<URI>
): [URI, T & NavigatableWidget][] {
const uris = Array.isArray(context) ? context : [context];
const result: [URI, T & NavigatableWidget][] = [];
for (const widget of widgets) {
if (NavigatableWidget.is(widget)) {
const resourceUri = widget.getResourceUri();
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
result.push([resourceUri, widget]);
}
}
}
return result;
}

View File

@@ -57,11 +57,9 @@ export class EditContributions extends Contribution {
execute: async () => { execute: async () => {
const value = await this.currentValue(); const value = await this.currentValue();
if (value !== undefined) { if (value !== undefined) {
this.clipboardService.writeText(` this.clipboardService.writeText(`\`\`\`cpp
\`\`\`cpp
${value} ${value}
\`\`\` \`\`\``);
`);
} }
}, },
}); });

View File

@@ -12,6 +12,7 @@ import {
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { import {
@@ -29,7 +30,6 @@ import {
CoreService, CoreService,
} from '../../common/protocol'; } from '../../common/protocol';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { unregisterSubmenu } from '../menu/arduino-menus';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@@ -37,7 +37,10 @@ export abstract class Examples extends SketchContribution {
private readonly commandRegistry: CommandRegistry; private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@@ -48,9 +51,6 @@ export abstract class Examples extends SketchContribution {
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
protected override init(): void { protected override init(): void {
@@ -58,12 +58,6 @@ export abstract class Examples extends SketchContribution {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard) this.handleBoardChanged(selectedBoard)
); );
this.notificationCenter.onDidReinitialize(() =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
// No force refresh. The core client was already refreshed.
})
);
} }
// 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
@@ -130,11 +124,6 @@ export abstract class Examples extends SketchContribution {
const { label } = sketchContainerOrPlaceholder; const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label]; submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions); this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
this.toDispose.push(
Disposable.create(() =>
unregisterSubmenu(submenuPath, this.menuRegistry)
)
);
sketches.push(...sketchContainerOrPlaceholder.sketches); sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children); children.push(...sketchContainerOrPlaceholder.children);
} else { } else {
@@ -201,7 +190,7 @@ export abstract class Examples extends SketchContribution {
private async clone(uri: string): Promise<Sketch | undefined> { private async clone(uri: string): Promise<Sketch | undefined> {
try { try {
const sketch = await this.sketchesService.cloneExample(uri); const sketch = await this.sketchService.cloneExample(uri);
return sketch; return sketch;
} catch (err) { } catch (err) {
if (SketchesError.NotFound.is(err)) { if (SketchesError.NotFound.is(err)) {
@@ -254,6 +243,9 @@ export class BuiltInExamples extends Examples {
@injectable() @injectable()
export class LibraryExamples extends Examples { export class LibraryExamples extends Examples {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void { override onStart(): void {

View File

@@ -17,7 +17,7 @@ import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core'; import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable() @injectable()
export class IncludeLibrary extends SketchContribution { export class IncludeLibrary extends SketchContribution {
@@ -53,7 +53,6 @@ export class IncludeLibrary extends SketchContribution {
this.notificationCenter.onLibraryDidUninstall(() => this.notificationCenter.onLibraryDidUninstall(() =>
this.updateMenuActions() this.updateMenuActions()
); );
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
} }
override async onReady(): Promise<void> { override async onReady(): Promise<void> {

View File

@@ -1,20 +1,15 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { inject, injectable } from '@theia/core/shared/inversify';
import { import {
ArduinoDaemon, ArduinoDaemon,
assertSanitizedFqbn,
BoardsService, BoardsService,
ExecutableService, ExecutableService,
sanitizeFqbn,
} from '../../common/protocol'; } from '../../common/protocol';
import { CurrentSketch } from '../sketches-service-client-impl'; import { HostedPluginEvents } from '../hosted-plugin-events';
import { SketchContribution, URI } from './contribution';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
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';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { SketchContribution, URI } from './contribution';
import { BoardsDataStore } from '../boards/boards-data-store';
@injectable() @injectable()
export class InoLanguage extends SketchContribution { export class InoLanguage extends SketchContribution {
@@ -33,15 +28,8 @@ export class InoLanguage extends SketchContribution {
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider; private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex();
private languageServerFqbn?: string; private languageServerFqbn?: string;
private languageServerStartMutex = new Mutex();
override onReady(): void { override onReady(): void {
const start = ( const start = (
@@ -55,61 +43,27 @@ export class InoLanguage extends SketchContribution {
} }
} }
}; };
const forceRestart = () => { this.boardsServiceProvider.onBoardsConfigChanged(start);
start(this.boardsServiceProvider.boardsConfig, true);
};
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigChanged(start),
this.hostedPluginEvents.onPluginsDidStart(() => this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig) start(this.boardsServiceProvider.boardsConfig)
), );
this.hostedPluginEvents.onPluginsWillUnload( this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined) () => (this.languageServerFqbn = undefined)
), );
this.preferences.onPreferenceChanged( this.preferences.onPreferenceChanged(
({ preferenceName, oldValue, newValue }) => { ({ preferenceName, oldValue, newValue }) => {
if (oldValue !== newValue) { if (oldValue !== newValue) {
switch (preferenceName) { switch (preferenceName) {
case 'arduino.language.log': case 'arduino.language.log':
case 'arduino.language.realTimeDiagnostics': case 'arduino.language.realTimeDiagnostics':
forceRestart(); start(this.boardsServiceProvider.boardsConfig, true);
} }
} }
} }
),
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onChanged((dataChangePerFqbn) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
); );
}
const matchingFqbn = dataChangePerFqbn.find(
(fqbn) => sanitizedFqbn === fqbn
);
const { boardsConfig } = this.boardsServiceProvider;
if (
matchingFqbn &&
boardsConfig.selectedBoard?.fqbn === matchingFqbn
) {
start(boardsConfig);
}
}
}),
]);
start(this.boardsServiceProvider.boardsConfig); start(this.boardsServiceProvider.boardsConfig);
} }
onStop(): void {
this.toDispose.dispose();
}
private async startLanguageServer( private async startLanguageServer(
fqbn: string, fqbn: string,
name: string | undefined, name: string | undefined,
@@ -147,18 +101,11 @@ export class InoLanguage extends SketchContribution {
} }
return; return;
} }
assertSanitizedFqbn(fqbn); if (!forceStart && fqbn === this.languageServerFqbn) {
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
// NOOP // NOOP
return; return;
} }
this.logger.info(`Starting language server: ${fqbnWithConfig}`); this.logger.info(`Starting language server: ${fqbn}`);
const log = this.preferences.get('arduino.language.log'); const log = this.preferences.get('arduino.language.log');
const realTimeDiagnostics = this.preferences.get( const realTimeDiagnostics = this.preferences.get(
'arduino.language.realTimeDiagnostics' 'arduino.language.realTimeDiagnostics'
@@ -194,7 +141,7 @@ export class InoLanguage extends SketchContribution {
log: currentSketchPath ? currentSketchPath : log, log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1', cliDaemonInstance: '1',
board: { board: {
fqbn: fqbnWithConfig, fqbn,
name: name ? `"${name}"` : undefined, name: name ? `"${name}"` : undefined,
}, },
realTimeDiagnostics, realTimeDiagnostics,
@@ -203,7 +150,7 @@ export class InoLanguage extends SketchContribution {
), ),
]); ]);
} catch (e) { } catch (e) {
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e); console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined; this.languageServerFqbn = undefined;
} finally { } finally {
release(); release();

View File

@@ -1,17 +1,31 @@
import { injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common'; import {
CommandRegistry,
DisposableCollection,
MaybePromise,
nls,
} from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings'; import { Settings } from '../dialogs/settings/settings';
import { MainMenuManager } from '../../common/main-menu-manager';
import debounce = require('lodash.debounce'); import debounce = require('lodash.debounce');
@injectable() @injectable()
export class InterfaceScale extends Contribution { export class InterfaceScale extends Contribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly menuActionsDisposables = new DisposableCollection();
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = { private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
increase: true, increase: true,
decrease: true, decrease: true,
@@ -48,22 +62,63 @@ export class InterfaceScale extends Contribution {
} }
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, { this.menuActionsDisposables.dispose();
const increaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id, commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize( label: nls.localize(
'arduino/editor/increaseFontSize', 'arduino/editor/increaseFontSize',
'Increase Font Size' 'Increase Font Size'
), ),
order: '0', order: '0',
}); };
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, { const decreaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id, commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize( label: nls.localize(
'arduino/editor/decreaseFontSize', 'arduino/editor/decreaseFontSize',
'Decrease Font Size' 'Decrease Font Size'
), ),
order: '1', order: '1',
}); };
if (this.fontScalingEnabled.increase) {
this.menuActionsDisposables.push(
registry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction.label,
{ order: increaseFontSizeMenuAction.order }
)
)
);
}
if (this.fontScalingEnabled.decrease) {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction.label,
{ order: decreaseFontSizeMenuAction.order }
)
)
);
}
this.mainMenuManager.update();
} }
private updateFontScalingEnabled(): void { private updateFontScalingEnabled(): void {
@@ -98,7 +153,7 @@ export class InterfaceScale extends Contribution {
); );
if (isChanged) { if (isChanged) {
this.fontScalingEnabled = fontScalingEnabled; this.fontScalingEnabled = fontScalingEnabled;
this.menuManager.update(); this.registerMenus(this.menuRegistry);
} }
} }

View File

@@ -1,39 +1,76 @@
import { DialogError } from '@theia/core/lib/browser/dialogs';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree'; import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
TaskFactoryImpl, Disposable,
WorkspaceInputDialogWithProgress, DisposableCollection,
} from '../theia/workspace/workspace-input-dialog'; } from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Progress,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
import { v4 } from 'uuid';
import { MainMenuManager } from '../../common/main-menu-manager';
import type { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { CreateApi } from '../create/create-api';
import { CreateUri } from '../create/create-uri';
import { Create } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree'; import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model'; import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands'; import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { Command, CommandRegistry, Sketch } from './contribution'; import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
CloudSketchContribution, import { Command, CommandRegistry, Contribution, URI } from './contribution';
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
@injectable() @injectable()
export class NewCloudSketch extends CloudSketchContribution { export class NewCloudSketch extends Contribution {
@inject(CreateApi)
private readonly createApi: CreateApi;
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly toDispose = new DisposableCollection(); private readonly toDispose = new DisposableCollection();
private _session: AuthenticationSession | undefined;
private _enabled: boolean;
override onReady(): void { override onReady(): void {
this.toDispose.pushAll([ this.toDispose.pushAll([
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()), this.authenticationService.onSessionDidChange((session) => {
this.createFeatures.onDidChangeSession(() => this.menuManager.update()), const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.mainMenuManager.update();
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.mainMenuManager.update();
}
}
}),
]); ]);
if (this.createFeatures.session) { this._enabled = this.preferences['arduino.cloud.enabled'];
this.menuManager.update(); this._session = this.authenticationService.session;
if (this._session) {
this.mainMenuManager.update();
} }
} }
@@ -43,16 +80,16 @@ export class NewCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, { registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(true), execute: () => this.createNewSketch(),
isEnabled: () => Boolean(this.createFeatures.session), isEnabled: () => !!this._session,
isVisible: () => this.createFeatures.enabled, isVisible: () => this._enabled,
}); });
} }
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id, commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'), label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
order: '1', order: '1',
}); });
} }
@@ -65,95 +102,153 @@ export class NewCloudSketch extends CloudSketchContribution {
} }
private async createNewSketch( private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined initialValue?: string | undefined
): Promise<void> { ): Promise<unknown> {
const treeModel = await this.treeModel(); const widget = await this.widgetContribution.widget;
if (treeModel) { const treeModel = this.treeModelFrom(widget);
const rootNode = treeModel.root; if (!treeModel) {
return this.openWizard( return undefined;
rootNode, }
treeModel, const rootNode = CompositeTreeNode.is(treeModel.root)
skipShowErrorMessageOnOpen, ? treeModel.root
initialValue : undefined;
if (!rootNode) {
return undefined;
}
return this.openWizard(rootNode, treeModel, initialValue);
}
private withProgress(
value: string,
treeModel: CloudSketchbookTreeModel
): (progress: Progress) => Promise<unknown> {
return async (progress: Progress) => {
let result: Create.Sketch | undefined | 'conflict';
try {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating remote sketch '{0}'...",
value
),
});
result = await this.createApi.createSketch(value);
} catch (err) {
if (isConflict(err)) {
result = 'conflict';
} else {
throw err;
}
} finally {
if (result) {
progress.report({
message: nls.localize(
'arduino/cloudSketch/synchronizing',
"Synchronizing sketchbook, pulling '{0}'...",
value
),
});
await treeModel.refresh();
}
}
if (result === 'conflict') {
return this.createNewSketch(value);
}
if (result) {
return this.open(treeModel, result);
}
return undefined;
};
}
private async open(
treeModel: CloudSketchbookTreeModel,
newSketch: Create.Sketch
): Promise<URI | undefined> {
const id = CreateUri.toUri(newSketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
); );
} }
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node });
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(
nls.localize(
'arduino/newCloudSketch/notFound',
"Could not pull the remote sketch '{0}'. It does not exist.",
newSketch.name
)
);
return undefined;
}
throw err;
}
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
);
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
const treeWidget = widget.getTreeWidget();
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
return undefined;
} }
private async openWizard( private async openWizard(
rootNode: CompositeTreeNode, rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel, treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined initialValue?: string | undefined
): Promise<void> { ): Promise<unknown> {
const existingNames = rootNode.children const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is) .filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name); .map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) => return new NewCloudSketchDialog(
this.createNewSketchWithProgress(treeModel, value)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
{ {
title: nls.localize( title: nls.localize(
'arduino/newCloudSketch/newSketchTitle', 'arduino/newCloudSketch/newSketchTitle',
'Name of the new Cloud Sketch' 'Name of a new Remote Sketch'
), ),
parentUri: CreateUri.root, parentUri: CreateUri.root,
initialValue, initialValue,
validate: (input) => { validate: (input) => {
if (existingNames.includes(input)) { if (existingNames.includes(input)) {
return sketchAlreadyExists(input); return nls.localize(
'arduino/newCloudSketch/sketchAlreadyExists',
"Remote sketch '{0}' already exists.",
input
);
} }
return Sketch.validateCloudSketchFolderName(input) ?? ''; // This is how https://create.arduino.cc/editor/ works when renaming a sketch.
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
return '';
}
return nls.localize(
'arduino/newCloudSketch/invalidSketchName',
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
);
}, },
}, },
this.labelProvider, this.labelProvider,
taskFactory (value) => this.withProgress(value, treeModel)
); ).open();
await dialog.open(skipShowErrorMessageOnOpen);
if (dialog.taskResult) {
this.openInNewWindow(dialog.taskResult);
}
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(false, taskFactory.value ?? initialValue);
}
throw err;
}
}
private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
return async (progress: Progress) => {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating cloud sketch '{0}'...",
value
),
});
const sketch = await this.createApi.createSketch(value);
progress.report({ message: synchronizingSketchbook });
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
return node;
};
}
private openInNewWindow(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<void> {
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
);
} }
} }
export namespace NewCloudSketch { export namespace NewCloudSketch {
@@ -163,3 +258,115 @@ export namespace NewCloudSketch {
}; };
} }
} }
function isConflict(err: unknown): boolean {
return isErrorWithStatusOf(err, 409);
}
function isNotFound(err: unknown): boolean {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is Error & { status: number } {
if (err instanceof Error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = err as any;
return 'status' in object && object.status === status;
}
return false;
}
@injectable()
class NewCloudSketchDialog extends WorkspaceInputDialog {
constructor(
@inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider,
private readonly withProgress: (
value: string
) => (progress: Progress) => Promise<unknown>
) {
super(props, labelProvider);
}
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
const spinner = document.createElement('div');
spinner.classList.add('spinner');
const disposables = new DisposableCollection();
try {
this.toggleButtons(true);
disposables.push(Disposable.create(() => this.toggleButtons(false)));
const closeParent = this.closeCrossNode.parentNode;
closeParent?.removeChild(this.closeCrossNode);
disposables.push(
Disposable.create(() => {
closeParent?.appendChild(this.closeCrossNode);
})
);
this.errorMessageNode.classList.add('progress');
disposables.push(
Disposable.create(() =>
this.errorMessageNode.classList.remove('progress')
)
);
const errorParent = this.errorMessageNode.parentNode;
errorParent?.insertBefore(spinner, this.errorMessageNode);
disposables.push(
Disposable.create(() => errorParent?.removeChild(spinner))
);
const cancellationSource = new CancellationTokenSource();
const progress: Progress = {
id: v4(),
cancel: () => cancellationSource.cancel(),
report: (update: ProgressUpdate) => {
this.setProgressMessage(update);
},
result: Promise.resolve(value),
};
await this.withProgress(value)(progress);
} finally {
disposables.dispose();
}
this.resolve(value);
Widget.detach(this);
}
}
private toggleButtons(disabled: boolean): void {
if (this.acceptButton) {
this.acceptButton.disabled = disabled;
}
if (this.closeButton) {
this.closeButton.disabled = disabled;
}
}
private setProgressMessage(update: ProgressUpdate): void {
if (update.work && update.work.done === update.work.total) {
this.errorMessageNode.innerText = '';
} else {
if (update.message) {
this.errorMessageNode.innerText = update.message;
}
}
}
}

View File

@@ -35,7 +35,7 @@ export class NewSketch extends SketchContribution {
async newSketch(): Promise<void> { async newSketch(): Promise<void> {
try { try {
const sketch = await this.sketchesService.createNewSketch(); const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri)); this.workspaceService.open(new URI(sketch.uri));
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());

View File

@@ -47,7 +47,7 @@ export class OpenRecentSketch extends SketchContribution {
} }
private update(forceUpdate?: boolean): void { private update(forceUpdate?: boolean): void {
this.sketchesService this.sketchService
.recentlyOpenedSketches(forceUpdate) .recentlyOpenedSketches(forceUpdate)
.then((sketches) => this.refreshMenu(sketches)); .then((sketches) => this.refreshMenu(sketches));
} }

View File

@@ -20,8 +20,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
export class OpenSketchFiles extends SketchContribution { export class OpenSketchFiles extends SketchContribution {
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, { registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI, focusMainSketchFile) => execute: (uri: URI) => this.openSketchFiles(uri),
this.openSketchFiles(uri, focusMainSketchFile),
}); });
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, { registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
execute: ( execute: (
@@ -34,19 +33,13 @@ export class OpenSketchFiles extends SketchContribution {
}); });
} }
private async openSketchFiles( private async openSketchFiles(uri: URI): Promise<void> {
uri: URI,
focusMainSketchFile = false
): Promise<void> {
try { try {
const sketch = await this.sketchesService.loadSketch(uri.toString()); const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch; const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) { for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri); await this.ensureOpened(uri);
} }
if (focusMainSketchFile) {
await this.ensureOpened(mainFileUri, true, { mode: 'activate' });
}
if (mainFileUri.endsWith('.pde')) { if (mainFileUri.endsWith('.pde')) {
const message = nls.localize( const message = nls.localize(
'arduino/common/oldFormat', 'arduino/common/oldFormat',
@@ -112,7 +105,7 @@ export class OpenSketchFiles extends SketchContribution {
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
const movedSketch = await promptMoveSketch(invalidMainSketchUri, { const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService, fileService: this.fileService,
sketchesService: this.sketchesService, sketchService: this.sketchService,
labelProvider: this.labelProvider, labelProvider: this.labelProvider,
}); });
if (movedSketch) { if (movedSketch) {
@@ -125,7 +118,7 @@ export class OpenSketchFiles extends SketchContribution {
} }
private async openFallbackSketch(): Promise<void> { private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchesService.createNewSketch(); const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true }); this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
} }
@@ -133,7 +126,7 @@ export class OpenSketchFiles extends SketchContribution {
uri: string, uri: string,
forceOpen = false, forceOpen = false,
options?: EditorOpenerOptions options?: EditorOpenerOptions
): Promise<EditorWidget | undefined> { ): Promise<unknown> {
const widget = this.editorManager.all.find( const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri (widget) => widget.editor.uri.toString() === uri
); );
@@ -191,24 +184,23 @@ export class OpenSketchFiles extends SketchContribution {
// The editor is expected to be attached to the shell and visible in the UI. // The editor is expected to be attached to the shell and visible in the UI.
// The deferred promise does not have to wait for the `editorManager#onCreated` event. // The deferred promise does not have to wait for the `editorManager#onCreated` event.
// It can resolve earlier. // It can resolve earlier.
if (widget) { if (!widget) {
deferred.resolve(editorWidget); deferred.resolve(editorWidget);
} }
}); });
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
const result: EditorWidget | undefined | 'timeout' = await Promise.race([ const result = await Promise.race([
deferred.promise, deferred.promise,
wait(timeout).then(() => { wait(timeout).then(() => {
disposables.dispose(); disposables.dispose();
return 'timeout' as const; return 'timeout';
}), }),
]); ]);
if (result === 'timeout') { if (result === 'timeout') {
console.warn( console.warn(
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}` `Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
); );
return undefined;
} }
return result; return result;
} }

View File

@@ -71,7 +71,7 @@ export class OpenSketch extends SketchContribution {
} }
const uri = SketchLocation.toUri(toOpen); const uri = SketchLocation.toUri(toOpen);
try { try {
await this.sketchesService.loadSketch(uri.toString()); await this.sketchService.loadSketch(uri.toString());
} catch (err) { } catch (err) {
if (SketchesError.NotFound.is(err)) { if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message); this.messageService.error(err.message);
@@ -82,7 +82,10 @@ export class OpenSketch extends SketchContribution {
} }
private async selectSketch(): Promise<Sketch | undefined> { private async selectSketch(): Promise<Sketch | undefined> {
const defaultPath = await this.defaultPath(); const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog( const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
@@ -106,14 +109,14 @@ export class OpenSketch extends SketchContribution {
} }
const sketchFilePath = filePaths[0]; const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath); const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri); const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) { if (sketch) {
return sketch; return sketch;
} }
if (Sketch.isSketchFile(sketchFileUri)) { if (Sketch.isSketchFile(sketchFileUri)) {
return promptMoveSketch(sketchFileUri, { return promptMoveSketch(sketchFileUri, {
fileService: this.fileService, fileService: this.fileService,
sketchesService: this.sketchesService, sketchService: this.sketchService,
labelProvider: this.labelProvider, labelProvider: this.labelProvider,
}); });
} }
@@ -132,11 +135,11 @@ export async function promptMoveSketch(
sketchFileUri: string | URI, sketchFileUri: string | URI,
options: { options: {
fileService: FileService; fileService: FileService;
sketchesService: SketchesService; sketchService: SketchesService;
labelProvider: LabelProvider; labelProvider: LabelProvider;
} }
): Promise<Sketch | undefined> { ): Promise<Sketch | undefined> {
const { fileService, sketchesService, labelProvider } = options; const { fileService, sketchService, labelProvider } = 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;
@@ -176,6 +179,6 @@ export async function promptMoveSketch(
uri, uri,
new URI(newSketchUri.resolve(nameWithExt).toString()) new URI(newSketchUri.resolve(nameWithExt).toString())
); );
return sketchesService.getSketchFolder(newSketchUri.toString()); return sketchService.getSketchFolder(newSketchUri.toString());
} }
} }

View File

@@ -1,166 +0,0 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import {
TaskFactoryImpl,
WorkspaceInputDialogWithProgress,
} from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import {
CloudSketchContribution,
pullingSketch,
pushingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch, URI } from './contribution';
export interface RenameCloudSketchParams {
readonly cloudUri: URI;
readonly sketch: Sketch;
}
@injectable()
export class RenameCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
execute: (params: RenameCloudSketchParams) =>
this.renameSketch(params, true),
});
}
private async renameSketch(
params: RenameCloudSketchParams,
skipShowErrorMessageOnOpen: boolean,
initValue: string = params.sketch.name
): Promise<string | undefined> {
const treeModel = await this.treeModel();
if (treeModel) {
const posixPath = params.cloudUri.path.toString();
const node = treeModel.getNode(posixPath);
const parentNode = node?.parent;
if (
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
CompositeTreeNode.is(parentNode)
) {
return this.openWizard(
params,
node,
parentNode,
treeModel,
skipShowErrorMessageOnOpen,
initValue
);
}
}
return undefined;
}
private async openWizard(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
parentNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
): Promise<string | undefined> {
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
? parentNode.uri
: CreateUri.root;
const existingNames = parentNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.renameSketchWithProgress(params, node, treeModel, value)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
{
title: nls.localize(
'arduino/renameCloudSketch/renameSketchTitle',
'New name of the Cloud Sketch'
),
parentUri,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return sketchAlreadyExists(input);
}
return Sketch.validateCloudSketchFolderName(input) ?? '';
},
},
this.labelProvider,
taskFactory
);
await dialog.open(skipShowErrorMessageOnOpen);
return dialog.taskResult;
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.renameSketch(
params,
false,
taskFactory.value ?? initialValue
);
}
throw err;
}
}
private renameSketchWithProgress(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
treeModel: CloudSketchbookTreeModel,
value: string
): (progress: Progress) => Promise<string | undefined> {
return async (progress: Progress) => {
const fromName = params.cloudUri.path.name;
const fromPosixPath = params.cloudUri.path.toString();
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node);
// rename
progress.report({
message: nls.localize(
'arduino/cloudSketch/renaming',
"Renaming cloud sketch from '{0}' to '{1}'...",
fromName,
value
),
});
await this.createApi.rename(fromPosixPath, toPosixPath);
// sync
progress.report({
message: synchronizingSketchbook,
});
this.createApi.sketchCache.init(); // invalidate the cache
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
if (!sketch) {
return undefined;
}
await treeModel.refresh();
// pull
progress.report({ message: pullingSketch(sketch.name) });
const pulledNode = await this.pull(sketch);
return pulledNode
? node.uri.parent.resolve(sketch.name).toString()
: undefined;
};
}
}
export namespace RenameCloudSketch {
export namespace Commands {
export const RENAME_CLOUD_SKETCH: Command = {
id: 'arduino-rename-cloud-sketch',
};
}
}

View File

@@ -1,34 +1,28 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
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 { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service'; import * as remote from '@theia/core/electron-shared/@electron/remote';
import { StartupTask } from '../../electron-common/startup-task'; import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import { import {
SketchContribution,
URI,
Command, Command,
CommandRegistry, CommandRegistry,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
Sketch, KeybindingRegistry,
URI,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import { StartupTask } from '../../electron-common/startup-task';
import { DeleteSketch } from './delete-sketch'; import { DeleteSketch } from './delete-sketch';
import {
RenameCloudSketch,
RenameCloudSketchParams,
} from './rename-cloud-sketch';
@injectable() @injectable()
export class SaveAsSketch extends CloudSketchContribution { export class SaveAsSketch extends SketchContribution {
@inject(ApplicationShell) @inject(ApplicationShell)
private readonly applicationShell: ApplicationShell; private readonly applicationShell: ApplicationShell;
@inject(WindowService) @inject(WindowService)
private readonly windowService: WindowService; private readonly windowService: WindowService;
@@ -41,7 +35,7 @@ export class SaveAsSketch extends CloudSketchContribution {
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: nls.localizeByDefault('Save As...'), label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
order: '7', order: '7',
}); });
} }
@@ -64,70 +58,21 @@ export class SaveAsSketch extends CloudSketchContribution {
markAsRecentlyOpened, markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> { ): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch(); const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return false; return false;
} }
let destinationUri: string | undefined; const isTemp = await this.sketchService.isTemp(sketch);
const cloudUri = this.createFeatures.cloudUri(sketch);
if (cloudUri) {
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
} else {
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
}
if (!destinationUri) {
return false;
}
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
destinationUri,
});
if (!newWorkspaceUri) {
return false;
}
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [{ toDelete: sketch.uri }],
});
}
this.workspaceService.open(new URI(newWorkspaceUri), options);
}
return !!newWorkspaceUri;
}
private async createCloudCopy(
params: RenameCloudSketchParams
): Promise<string | undefined> {
return this.commandService.executeCommand<string>(
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
params
);
}
private async createLocalCopy(
sketch: Sketch,
execOnlyIfTemp?: boolean
): Promise<string | undefined> {
const isTemp = await this.sketchesService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) { if (!isTemp && !!execOnlyIfTemp) {
return undefined; return false;
} }
const sketchUri = new URI(sketch.uri); const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = await this.defaultUri(); const sketchbookDirUri = new URI(configuration.sketchDirUri);
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI. // If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location. // If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch. // Otherwise, it proposes the parent folder of the current sketch.
@@ -142,26 +87,13 @@ export class SaveAsSketch extends CloudSketchContribution {
// If target does not exist, propose a `directories.user`/${sketch.name} path // If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss} // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
const defaultUri = containerDirUri.resolve( const defaultUri = containerDirUri.resolve(
Sketch.toValidSketchFolderName(sketch.name, exists) exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name
); );
const defaultPath = await this.fileService.fsPath(defaultUri); const defaultPath = await this.fileService.fsPath(defaultUri);
return await this.promptLocalSketchFolderDestination(sketch, defaultPath); const { filePath, canceled } = await remote.dialog.showSaveDialog(
}
/**
* Prompts for the new sketch folder name until a valid one is give,
* then resolves with the destination sketch folder URI string,
* or `undefined` if the operation was canceled.
*/
private async promptLocalSketchFolderDestination(
sketch: Sketch,
defaultPath: string
): Promise<string | undefined> {
let sketchFolderDestinationUri: string | undefined;
while (!sketchFolderDestinationUri) {
const { filePath } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
title: nls.localize( title: nls.localize(
@@ -171,107 +103,61 @@ export class SaveAsSketch extends CloudSketchContribution {
defaultPath, defaultPath,
} }
); );
if (!filePath) { if (!filePath || canceled) {
return undefined; return false;
} }
const destinationUri = await this.fileSystemExt.getUri(filePath); const destinationUri = await this.fileSystemExt.getUri(filePath);
// The new location of the sketch cannot be inside the location of current sketch. if (!destinationUri) {
// https://github.com/arduino/arduino-ide/issues/1882 return false;
let dialogContent: InvalidSketchFolderDialogContent | undefined; }
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) { const workspaceUri = await this.sketchService.copy(sketch, {
dialogContent = { destinationUri,
message: nls.localize( });
'arduino/sketch/invalidSketchFolderLocationMessage', if (workspaceUri) {
"Invalid sketch folder location: '{0}'", await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
filePath if (markAsRecentlyOpened) {
), this.sketchService.markAsRecentlyOpened(workspaceUri);
details: nls.localize( }
'arduino/sketch/invalidSketchFolderLocationDetails', }
'You cannot save a sketch into a folder inside itself.' const options: WorkspaceInput & StartupTask.Owner = {
), preserveWindow: true,
question: nls.localize( tasks: [],
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
'Do you want to try saving the sketch to a different location?'
),
}; };
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
});
} }
if (!dialogContent) { this.workspaceService.open(new URI(workspaceUri), options);
const sketchFolderName = new URI(destinationUri).path.base;
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
if (errorMessage) {
dialogContent = {
message: nls.localize(
'arduino/sketch/invalidSketchFolderNameMessage',
"Invalid sketch folder name: '{0}'",
sketchFolderName
),
details: errorMessage,
question: nls.localize(
'arduino/sketch/editInvalidSketchFolderQuestion',
'Do you want to try saving the sketch with a different name?'
),
};
} }
} return !!workspaceUri;
if (dialogContent) {
const message = `
${dialogContent.message}
${dialogContent.details}
${dialogContent.question}`.trim();
defaultPath = filePath;
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message,
buttons: [Dialog.CANCEL, Dialog.YES],
}
);
// cancel
if (response === 0) {
return undefined;
}
} else {
sketchFolderDestinationUri = destinationUri;
}
}
return sketchFolderDestinationUri;
} }
private async saveOntoCopiedSketch( private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
sketch: Sketch,
newSketchFolderUri: string
): Promise<void> {
const widgets = this.applicationShell.widgets; const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>(); const snapshots = new Map<string, object>();
for (const widget of widgets) { for (const widget of widgets) {
const saveable = Saveable.getDirty(widget); const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget); const uri = NavigatableWidget.getUri(widget);
if (!uri) { const uriString = uri?.toString();
continue;
}
const uriString = uri.toString();
let relativePath: string; let relativePath: string;
if ( if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
uriString.includes(sketch.uri) &&
saveable &&
saveable.createSnapshot
) {
// The main file will change its name during the copy process // The main file will change its name during the copy process
// We need to store the new name in the map // We need to store the new name in the map
if (sketch.mainFileUri === uriString) { if (mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext; const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
relativePath = '/' + lastPart; relativePath = '/' + lastPart;
} else { } else {
relativePath = uri.toString().substring(sketch.uri.length); relativePath = uri.toString().substring(sketchUri.length);
} }
snapshots.set(relativePath, saveable.createSnapshot()); snapshots.set(relativePath, saveable.createSnapshot());
} }
} }
await Promise.all( await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
Array.from(snapshots.entries()).map(async ([path, snapshot]) => { const widgetUri = new URI(newSketchUri + path);
const widgetUri = new URI(newSketchFolderUri + path);
try { try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri); const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget); const saveable = Saveable.get(widget);
@@ -282,17 +168,10 @@ ${dialogContent.question}`.trim();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}) }));
);
} }
} }
interface InvalidSketchFolderDialogContent {
readonly message: string;
readonly details: string;
readonly question: string;
}
export namespace SaveAsSketch { export namespace SaveAsSketch {
export namespace Commands { export namespace Commands {
export const SAVE_AS_SKETCH: Command = { export const SAVE_AS_SKETCH: Command = {

View File

@@ -10,7 +10,7 @@ import {
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable() @injectable()
export class SaveSketch extends SketchContribution { export class SaveSketch extends SketchContribution {
@@ -40,7 +40,7 @@ export class SaveSketch extends SketchContribution {
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return; return;
} }
const isTemp = await this.sketchesService.isTemp(sketch); const isTemp = await this.sketchService.isTemp(sketch);
if (isTemp) { if (isTemp) {
return this.commandService.executeCommand( return this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id, SaveAsSketch.Commands.SAVE_AS_SKETCH.id,

View File

@@ -1,34 +1,32 @@
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 type { Settings } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
Command, Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry,
SketchContribution, SketchContribution,
KeybindingRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class OpenSettings extends SketchContribution { export class Settings extends SketchContribution {
@inject(SettingsDialog) @inject(SettingsDialog)
private readonly settingsDialog: SettingsDialog; protected readonly settingsDialog: SettingsDialog;
private settingsOpened = false; protected settingsOpened = false;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSettings.Commands.OPEN, { registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => { execute: async () => {
let settings: Settings | undefined = undefined; let settings: Preferences | undefined = undefined;
try { try {
this.settingsOpened = true; this.settingsOpened = true;
this.menuManager.update();
settings = await this.settingsDialog.open(); settings = await this.settingsDialog.open();
} finally { } finally {
this.settingsOpened = false; this.settingsOpened = false;
this.menuManager.update();
} }
if (settings) { if (settings) {
await this.settingsService.update(settings); await this.settingsService.update(settings);
@@ -43,7 +41,7 @@ export class OpenSettings extends SketchContribution {
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: OpenSettings.Commands.OPEN.id, commandId: Settings.Commands.OPEN.id,
label: label:
nls.localize( nls.localize(
'vscode/preferences.contribution/preferences', 'vscode/preferences.contribution/preferences',
@@ -59,13 +57,13 @@ export class OpenSettings extends SketchContribution {
override registerKeybindings(registry: KeybindingRegistry): void { override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: OpenSettings.Commands.OPEN.id, command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,', keybinding: 'CtrlCmd+,',
}); });
} }
} }
export namespace OpenSettings { export namespace Settings {
export namespace Commands { export namespace Commands {
export const OPEN: Command = { export const OPEN: Command = {
id: 'arduino-settings-open', id: 'arduino-settings-open',

View File

@@ -1,34 +1,50 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { import {
URI,
SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
open, KeybindingRegistry,
SketchContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
URI, open,
} from './contribution'; } from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class SketchControl extends SketchContribution { export class SketchControl extends SketchContribution {
@inject(ApplicationShell) @inject(ApplicationShell)
private readonly shell: ApplicationShell; protected readonly shell: ApplicationShell;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
private readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
protected readonly toDisposeBeforeCreateNewContextMenu = protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection(); new DisposableCollection();
@@ -41,23 +57,39 @@ export class SketchControl extends SketchContribution {
this.shell.getWidgets('main').indexOf(widget) !== -1, this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => { execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose(); this.toDisposeBeforeCreateNewContextMenu.dispose();
let parentElement: HTMLElement | undefined = undefined;
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
);
if (target instanceof HTMLElement) {
parentElement = target.parentElement ?? undefined;
}
if (!parentElement) {
return;
}
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return; return;
} }
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
);
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowRename(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
@@ -73,11 +105,32 @@ export class SketchControl extends SketchContribution {
) )
) )
); );
} else {
const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/rename', 'Rename')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
)
);
}
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowDelete(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
commandId: WorkspaceCommands.FILE_DELETE.id, commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: nls.localize('vscode/fileActions/delete', 'Delete'), label: nls.localize('vscode/fileActions/delete', 'Delete'),
order: '2', order: '2',
} }
@@ -89,9 +142,22 @@ export class SketchControl extends SketchContribution {
) )
) )
); );
} else {
const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/delete', 'Delete')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
)
);
}
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
for (let i = 0; i < uris.length; i++) { for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]); const uri = new URI(uris[i]);
@@ -127,7 +193,6 @@ export class SketchControl extends SketchContribution {
parentElement.getBoundingClientRect().top + parentElement.getBoundingClientRect().top +
parentElement.offsetHeight, parentElement.offsetHeight,
}, },
showDisabled: true,
}; };
this.contextMenuRenderer.render(options); this.contextMenuRenderer.render(options);
}, },
@@ -170,7 +235,7 @@ export class SketchControl extends SketchContribution {
}); });
registry.registerKeybinding({ registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id, command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
}); });
registry.registerKeybinding({ registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id, command: CommonCommands.NEXT_TAB.id,
@@ -184,6 +249,27 @@ export class SketchControl extends SketchContribution {
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id, command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
}); });
} }
protected isCloudSketch(uri: string): boolean {
try {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
} catch {
return false;
}
}
protected allowRename(uri: string): boolean {
return !this.isCloudSketch(uri);
}
protected allowDelete(uri: string): boolean {
return !this.isCloudSketch(uri);
}
} }
export namespace SketchControl { export namespace SketchControl {

View File

@@ -3,7 +3,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileChangeType } from '@theia/filesystem/lib/common/files'; import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution } from './contribution'; import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files'; import { OpenSketchFiles } from './open-sketch-files';
@@ -38,7 +38,7 @@ export class SketchFilesTracker extends SketchContribution {
type === FileChangeType.ADDED && type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri resource.parent.toString() === sketch.uri
) { ) {
const reloadedSketch = await this.sketchesService.loadSketch( const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri sketch.uri
); );
if (Sketch.isInSketch(resource, reloadedSketch)) { if (Sketch.isInSketch(resource, reloadedSketch)) {

View File

@@ -11,7 +11,6 @@ import { nls } from '@theia/core/lib/common/nls';
export class Sketchbook extends Examples { export class Sketchbook extends Examples {
override onStart(): void { override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update()); this.sketchServiceClient.onSketchbookDidChange(() => this.update());
this.configService.onDidChangeSketchDirUri(() => this.update());
} }
override async onReady(): Promise<void> { override async onReady(): Promise<void> {
@@ -19,7 +18,7 @@ export class Sketchbook extends Examples {
} }
protected override update(): void { protected override update(): void {
this.sketchesService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.menuManager.update(); this.menuManager.update();
}); });

View File

@@ -12,6 +12,7 @@ import {
PreferenceScope, PreferenceScope,
PreferenceService, PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service'; } from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { import {
arduinoCert, arduinoCert,
certificateList, certificateList,
@@ -30,29 +31,22 @@ export class UploadCertificate extends Contribution {
@inject(PreferenceService) @inject(PreferenceService)
protected readonly preferenceService: PreferenceService; protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader) @inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false; protected dialogOpened = false;
override onStart(): void {
this.preferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.board.certificates') {
this.menuManager.update();
}
});
}
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, { registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => { execute: async () => {
try { try {
this.dialogOpened = true; this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open(); await this.dialog.open();
} finally { } finally {
this.dialogOpened = false; this.dialogOpened = false;
this.menuManager.update();
} }
}, },
isEnabled: () => !this.dialogOpened, isEnabled: () => !this.dialogOpened,
@@ -60,7 +54,7 @@ export class UploadCertificate extends Contribution {
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, { registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => { execute: async (certToRemove) => {
const certs = this.preferences.get('arduino.board.certificates'); const certs = this.arduinoPreferences.get('arduino.board.certificates');
this.preferenceService.set( this.preferenceService.set(
'arduino.board.certificates', 'arduino.board.certificates',
@@ -81,6 +75,7 @@ export class UploadCertificate extends Contribution {
.join(' ')}` .join(' ')}`
); );
}, },
isEnabled: () => true,
}); });
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, { registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
@@ -94,6 +89,7 @@ export class UploadCertificate extends Contribution {
args: [args.cert], args: [args.cert],
}); });
}, },
isEnabled: () => true,
}); });
} }

View File

@@ -21,11 +21,9 @@ export class UploadFirmware extends Contribution {
execute: async () => { execute: async () => {
try { try {
this.dialogOpened = true; this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open(); await this.dialog.open();
} finally { } finally {
this.dialogOpened = false; this.dialogOpened = false;
this.menuManager.update();
} }
}, },
isEnabled: () => !this.dialogOpened, isEnabled: () => !this.dialogOpened,

View File

@@ -1,6 +1,6 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { CoreService, Port, sanitizeFqbn } from '../../common/protocol'; import { CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
@@ -12,7 +12,7 @@ import {
CoreServiceContribution, CoreServiceContribution,
} from './contribution'; } from './contribution';
import { deepClone, nls } from '@theia/core/lib/common'; import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch'; import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields'; import { UserFields } from './user-fields';
@@ -106,7 +106,6 @@ export class UploadSketch extends CoreServiceContribution {
// toggle the toolbar button and menu item state. // toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not // uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true; this.uploadInProgress = true;
this.menuManager.update();
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload(); this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
this.clearVisibleNotification(); this.clearVisibleNotification();
@@ -151,7 +150,6 @@ export class UploadSketch extends CoreServiceContribution {
this.handleError(e); this.handleError(e);
} finally { } finally {
this.uploadInProgress = false; this.uploadInProgress = false;
this.menuManager.update();
this.boardsServiceProvider.attemptPostUploadAutoSelect(); this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
} }
@@ -170,7 +168,7 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([ await Promise.all([
verifyOptions.fqbn, // already decorated FQBN verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)), this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
this.preferences.get('arduino.upload.verify'), this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'), this.preferences.get('arduino.upload.verbose'),
]); ]);
@@ -207,6 +205,19 @@ export class UploadSketch extends CoreServiceContribution {
} }
return port; return port;
} }
/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
}
} }
export namespace UploadSketch { export namespace UploadSketch {

View File

@@ -1,9 +1,9 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { nls } from '@theia/core/lib/common'; import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol'; import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution'; import { MenuModelRegistry, Contribution } from './contribution';
import { UploadSketch } from './upload-sketch'; import { UploadSketch } from './upload-sketch';
@@ -12,6 +12,7 @@ export class UserFields extends Contribution {
private boardRequiresUserFields = false; private boardRequiresUserFields = false;
private userFieldsSet = false; private userFieldsSet = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map(); private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
@inject(UserFieldsDialog) @inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog; private readonly userFieldsDialog: UserFieldsDialog;
@@ -19,22 +20,42 @@ export class UserFields extends Contribution {
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider; private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected override init(): void { protected override init(): void {
super.init(); super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => { this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields = const userFields =
await this.boardsServiceProvider.selectedBoardUserFields(); await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0; this.boardRequiresUserFields = userFields.length > 0;
this.menuManager.update(); this.registerMenus(this.menuRegistry);
}); });
} }
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2', order: '2',
}); })
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
} }
private selectedFqbnAddress(): string | undefined { private selectedFqbnAddress(): string | undefined {

View File

@@ -1,202 +0,0 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import { Sketch, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
@injectable()
export class ValidateSketch extends CloudSketchContribution {
override onReady(): void {
this.validate();
}
private async validate(): Promise<void> {
const result = await this.promptFixActions();
if (!result) {
const yes = await this.prompt(
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
nls.localize(
'arduino/validateSketch/abortFixMessage',
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
Dialog.NO
),
[Dialog.NO, Dialog.YES]
);
if (yes) {
return this.validate();
}
const sketch = await this.sketchesService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), {
preserveWindow: true,
});
}
}
/**
* Returns with an array of actions the user has to perform to fix the invalid sketch.
*/
private validateSketch(
sketch: Sketch,
dataDirUri: URI | undefined
): FixAction[] {
// sketch code file validation errors first as they do not require window reload
const actions = Sketch.uris(sketch)
.filter((uri) => uri !== sketch.mainFileUri)
.map((uri) => new URI(uri))
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
.map((uri) => ({
uri,
error: this.doValidate(sketch, dataDirUri, uri.path.name),
}))
.filter(({ error }) => Boolean(error))
.map((object) => <{ uri: URI; error: string }>object)
.map(({ uri, error }) => ({
execute: async () => {
const unknown =
(await this.promptRenameSketchFile(uri, error)) &&
(await this.commandService.executeCommand(
WorkspaceCommands.FILE_RENAME.id,
uri
));
return !!unknown;
},
}));
// sketch folder + main sketch file last as it requires a `Save as...` and the window reload
const sketchFolderName = new URI(sketch.uri).path.base;
const sketchFolderNameError = this.doValidate(
sketch,
dataDirUri,
sketchFolderName
);
if (sketchFolderNameError) {
actions.push({
execute: async () => {
const unknown =
(await this.promptRenameSketch(sketch, sketchFolderNameError)) &&
(await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
<SaveAsSketch.Options>{
markAsRecentlyOpened: true,
openAfterMove: true,
wipeOriginal: true,
}
));
return !!unknown;
},
});
}
return actions;
}
private doValidate(
sketch: Sketch,
dataDirUri: URI | undefined,
toValidate: string
): string | undefined {
const cloudUri = this.createFeatures.isCloud(sketch, dataDirUri);
return cloudUri
? Sketch.validateCloudSketchFolderName(toValidate)
: Sketch.validateSketchFolderName(toValidate);
}
private async currentSketch(): Promise<Sketch> {
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
if (CurrentSketch.isValid(sketch)) {
return sketch;
}
const deferred = new Deferred<Sketch>();
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
(sketch) => {
if (CurrentSketch.isValid(sketch)) {
disposable.dispose();
deferred.resolve(sketch);
}
}
);
return deferred.promise;
}
private async promptFixActions(): Promise<boolean> {
const maybeDataDirUri = this.configService.tryGetDataDirUri();
const [sketch, dataDirUri] = await Promise.all([
this.currentSketch(),
maybeDataDirUri ??
waitForEvent(this.configService.onDidChangeDataDirUri, 5_000),
]);
const fixActions = this.validateSketch(sketch, dataDirUri);
for (const fixAction of fixActions) {
const result = await fixAction.execute();
if (!result) {
return false;
}
}
return true;
}
private async promptRenameSketch(
sketch: Sketch,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFolderTitle',
'Invalid sketch name'
),
nls.localize(
'arduino/validateSketch/renameSketchFolderMessage',
"The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
sketch.name,
error
)
);
}
private async promptRenameSketchFile(
uri: URI,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFileTitle',
'Invalid sketch filename'
),
nls.localize(
'arduino/validateSketch/renameSketchFileMessage',
"The sketch file '{0}' cannot be used. {1} Do you want to rename the sketch file now?",
uri.path.base,
error
)
);
}
private async prompt(
title: string,
message: string,
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
): Promise<boolean> {
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
title,
message,
type: 'warning',
buttons,
}
);
// cancel
if (response === 0) {
return false;
}
return true;
}
}
interface FixAction {
execute(): Promise<boolean>;
}

View File

@@ -11,7 +11,7 @@ import {
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CoreService } from '../../common/protocol'; import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler'; import { CoreErrorHandler } from './core-error-handler';
@@ -21,18 +21,11 @@ export interface VerifySketchParams {
*/ */
readonly exportBinaries?: boolean; readonly exportBinaries?: boolean;
/** /**
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default. * If `true`, there won't be any UI indication of the verify command. It's `false` by default.
*/ */
readonly silent?: boolean; readonly silent?: boolean;
} }
/**
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
*/
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
@injectable() @injectable()
export class VerifySketch extends CoreServiceContribution { export class VerifySketch extends CoreServiceContribution {
@inject(CoreErrorHandler) @inject(CoreErrorHandler)
@@ -40,24 +33,22 @@ export class VerifySketch extends CoreServiceContribution {
private readonly onDidChangeEmitter = new Emitter<void>(); private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event; private readonly onDidChange = this.onDidChangeEmitter.event;
private verifyProgress: VerifyProgress = 'idle'; private verifyInProgress = false;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: (params?: VerifySketchParams) => this.verifySketch(params), execute: (params?: VerifySketchParams) => this.verifySketch(params),
isEnabled: () => this.verifyProgress === 'idle', isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch({ exportBinaries: true }), execute: () => this.verifySketch({ exportBinaries: true }),
isEnabled: () => this.verifyProgress === 'idle', isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => this.verifyProgress !== 'explicit-verify', isEnabled: () => !this.verifyInProgress,
// toggled only when verify is running, but not toggled when automatic verify is running before the upload isToggled: () => this.verifyInProgress,
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit-verify',
execute: () => execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id), registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
}); });
@@ -108,16 +99,15 @@ export class VerifySketch extends CoreServiceContribution {
private async verifySketch( private async verifySketch(
params?: VerifySketchParams params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> { ): Promise<CoreService.Options.Compile | undefined> {
if (this.verifyProgress !== 'idle') { if (this.verifyInProgress) {
return undefined; return undefined;
} }
try { try {
this.verifyProgress = params?.silent if (!params?.silent) {
? 'automatic-verify' this.verifyInProgress = true;
: 'explicit-verify';
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
this.menuManager.update(); }
this.clearVisibleNotification(); this.clearVisibleNotification();
this.coreErrorHandler.reset(); this.coreErrorHandler.reset();
@@ -149,9 +139,10 @@ export class VerifySketch extends CoreServiceContribution {
this.handleError(e); this.handleError(e);
return undefined; return undefined;
} finally { } finally {
this.verifyProgress = 'idle'; this.verifyInProgress = false;
if (!params?.silent) {
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
this.menuManager.update(); }
} }
} }

View File

@@ -1,16 +1,12 @@
import { MaybePromise } from '@theia/core/lib/common/types'; import { injectable, inject } from '@theia/core/shared/inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { fetch } from 'cross-fetch';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import * as createPaths from './create-paths'; import * as createPaths from './create-paths';
import { posix } from './create-paths'; import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import { Create, CreateError } from './typings'; import { Create, CreateError } from './typings';
export interface ResponseResultProvider { export interface ResponseResultProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(response: Response): Promise<any>; (response: Response): Promise<any>;
} }
export namespace ResponseResultProvider { export namespace ResponseResultProvider {
@@ -19,8 +15,6 @@ export namespace ResponseResultProvider {
export const JSON: ResponseResultProvider = (response) => response.json(); export const JSON: ResponseResultProvider = (response) => response.json();
} }
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
export function Utf8ArrayToStr(array: Uint8Array): string { export function Utf8ArrayToStr(array: Uint8Array): string {
let out, i, c; let out, i, c;
let char2, char3; let char2, char3;
@@ -67,13 +61,20 @@ type ResourceType = 'f' | 'd';
@injectable() @injectable()
export class CreateApi { export class CreateApi {
@inject(SketchCache) @inject(SketchCache)
readonly sketchCache: SketchCache; protected sketchCache: SketchCache;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService; protected authenticationService: AuthenticationClientService;
@inject(ArduinoPreferences) protected arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesService) public init(
private readonly sketchesService: SketchesService; authenticationService: AuthenticationClientService,
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this;
}
getSketchSecretStat(sketch: Create.Sketch): Create.Resource { getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
return { return {
@@ -128,13 +129,10 @@ export class CreateApi {
async createSketch( async createSketch(
posixPath: string, posixPath: string,
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent() content: string = CreateApi.defaultInoContent
): Promise<Create.Sketch> { ): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`); const url = new URL(`${this.domain()}/sketches`);
const [headers, content] = await Promise.all([ const headers = await this.headers();
this.headers(),
contentProvider,
]);
const payload = { const payload = {
ino: btoa(content), ino: btoa(content),
path: posixPath, path: posixPath,
@@ -293,7 +291,7 @@ export class CreateApi {
this.sketchCache.addSketch(sketch); this.sketchCache.addSketch(sketch);
let file = ''; let file = '';
if (sketch.secrets) { if (sketch && sketch.secrets) {
for (const item of sketch.secrets) { for (const item of sketch.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`; file += `#define ${item.name} "${item.value}"\r\n`;
} }
@@ -383,7 +381,7 @@ export class CreateApi {
return; return;
} }
// do not upload "do_not_sync" files/directories and their descendants // do not upload "do_not_sync" files/directoris and their descendants
const segments = posixPath.split(posix.sep) || []; const segments = posixPath.split(posix.sep) || [];
if ( if (
segments.some((segment) => Create.do_not_sync_files.includes(segment)) segments.some((segment) => Create.do_not_sync_files.includes(segment))
@@ -417,21 +415,6 @@ export class CreateApi {
await this.delete(posixPath, 'd'); await this.delete(posixPath, 'd');
} }
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
* instead of 404 when deleting a non-existing resource.
*/
async deleteSketch(sketchPath: string): Promise<void> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
await this.run(url, {
method: 'DELETE',
headers,
});
}
private async delete(posixPath: string, type: ResourceType): Promise<void> { private async delete(posixPath: string, type: ResourceType): Promise<void> {
const url = new URL( const url = new URL(
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}` `${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
@@ -492,12 +475,14 @@ export class CreateApi {
} }
private async run<T>( private async run<T>(
requestInfo: URL, requestInfo: RequestInfo | URL,
init: RequestInit | undefined, init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> { ): Promise<T> {
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`); const response = await fetch(
const response = await fetch(requestInfo.toString(), init); requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
init
);
if (!response.ok) { if (!response.ok) {
let details: string | undefined = undefined; let details: string | undefined = undefined;
try { try {
@@ -531,3 +516,19 @@ export class CreateApi {
return this.authenticationService.session?.accessToken || ''; return this.authenticationService.session?.accessToken || '';
} }
} }
export namespace CreateApi {
export const defaultInoContent = `/*
*/
void setup() {
}
void loop() {
}
`;
}

View File

@@ -1,95 +0,0 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Sketch } from '../../common/protocol';
import { AuthenticationSession } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
@injectable()
export class CreateFeatures implements FrontendApplicationContribution {
@inject(ArduinoPreferences)
private readonly preferences: ArduinoPreferences;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
private readonly onDidChangeSessionEmitter = new Emitter<
AuthenticationSession | undefined
>();
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeSessionEmitter,
this.onDidChangeEnabledEmitter
);
private _enabled: boolean;
private _session: AuthenticationSession | undefined;
onStart(): void {
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange((session) => {
const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.onDidChangeSessionEmitter.fire(this._session);
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.onDidChangeEnabledEmitter.fire(this._enabled);
}
}
}),
]);
this._enabled = this.preferences['arduino.cloud.enabled'];
this._session = this.authenticationService.session;
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
return this.onDidChangeSessionEmitter.event;
}
get onDidChangeEnabled(): Event<boolean> {
return this.onDidChangeEnabledEmitter.event;
}
get enabled(): boolean {
return this._enabled;
}
get session(): AuthenticationSession | undefined {
return this._session;
}
/**
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
* Returns with `undefined` if `dataDirUri` is `undefined`.
*/
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
if (!dataDirUri) {
console.warn(
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
);
return undefined;
}
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
}
cloudUri(sketch: Sketch): URI | undefined {
if (!this.session) {
return undefined;
}
return this.localCacheFsProvider.from(new URI(sketch.uri));
}
}

View File

@@ -189,6 +189,10 @@ export class CreateFsProvider
FileSystemProviderErrorCode.NoPermissions FileSystemProviderErrorCode.NoPermissions
); );
} }
return this.createApi;
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
} }
} }

View File

@@ -1,4 +1,4 @@
import { URI as Uri } from '@theia/core/shared/vscode-uri'; import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { toPosixPath, parentPosix, posix } from './create-paths'; import { toPosixPath, parentPosix, posix } from './create-paths';
import { Create } from './typings'; import { Create } from './typings';

View File

@@ -71,23 +71,3 @@ export class CreateError extends Error {
Object.setPrototypeOf(this, CreateError.prototype); Object.setPrototypeOf(this, CreateError.prototype);
} }
} }
export type ConflictError = CreateError & { status: 409 };
export function isConflict(err: unknown): err is ConflictError {
return isErrorWithStatusOf(err, 409);
}
export type NotFoundError = CreateError & { status: 404 };
export function isNotFound(err: unknown): err is NotFoundError {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is CreateError & { status: number } {
if (err instanceof CreateError) {
return err.status === status;
}
return false;
}

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#dae3e3", "editor.foreground": "#dae3e3",
"editor.lineHighlightBackground": "#434f5410", "editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#00818480", "editor.selectionBackground": "#00818480",
"editorCursor.foreground": "#dae3e3", "editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf", "editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#171e21", "editorWidget.background": "#171e21",
"editorWidget.foreground": "#dae3e3", "editorWidget.foreground": "#dae3e3",
@@ -67,8 +67,7 @@
"tree.indentGuidesStroke": "#374146", "tree.indentGuidesStroke": "#374146",
"tab.unfocusedActiveForeground": "#dae3e3", "tab.unfocusedActiveForeground": "#dae3e3",
"tab.inactiveBackground": "#171e21", "tab.inactiveBackground": "#171e21",
"textLink.foreground": "#0ca1a6", "textLink.foreground": "#0ca1a6"
"errorForeground": "#df7365"
}, },
"tokenColors": [ "tokenColors": [
{ {

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#4e5b61", "editor.foreground": "#4e5b61",
"editor.lineHighlightBackground": "#434f5410", "editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#7fcbcdb3", "editor.selectionBackground": "#7fcbcdb3",
"editorCursor.foreground": "#4e5b61", "editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf", "editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#f7f9f9", "editorWidget.background": "#f7f9f9",
"editorWidget.foreground": "#4e5b61", "editorWidget.foreground": "#4e5b61",
@@ -67,8 +67,7 @@
"tree.indentGuidesStroke": "#dae3e3", "tree.indentGuidesStroke": "#dae3e3",
"tab.unfocusedActiveForeground": "#4e5b61", "tab.unfocusedActiveForeground": "#4e5b61",
"tab.inactiveBackground": "#ecf1f1", "tab.inactiveBackground": "#ecf1f1",
"textLink.foreground": "#008184", "textLink.foreground": "#008184"
"errorForeground": "#df7365"
}, },
"tokenColors": [ "tokenColors": [
{ {

View File

@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets'; import { Widget } from '@theia/core/shared/@phosphor/widgets';
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 { clipboard } from 'electron';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser'; import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs'; import { AbstractDialog } from '../theia/dialogs/dialogs';
import { CreateApi } from '../create/create-api'; import { CreateApi } from '../create/create-api';

View File

@@ -5,8 +5,10 @@ import {
postConstruct, postConstruct,
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs'; import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { ReactDialog } from '../../theia/dialogs/dialogs'; import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging'; import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { import {
AvailableBoard, AvailableBoard,
BoardsServiceProvider, BoardsServiceProvider,
@@ -21,30 +23,26 @@ import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable() @injectable()
export class UploadFirmwareDialogProps extends DialogProps {} export class UploadFirmwareDialogWidget extends ReactWidget {
@injectable()
export class UploadFirmwareDialog extends ReactDialog<void> {
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
private readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader) @inject(ArduinoFirmwareUploader)
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader; protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService; private readonly appStatusService: FrontendApplicationStateService;
private updatableFqbns: string[] = []; protected updatableFqbns: string[] = [];
private availableBoards: AvailableBoard[] = []; protected availableBoards: AvailableBoard[] = [];
private isOpen = new Object(); protected isOpen = new Object();
private busy = false;
constructor( public busyCallback = (busy: boolean) => {
@inject(UploadFirmwareDialogProps) return;
protected override readonly props: UploadFirmwareDialogProps };
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' }); constructor() {
this.node.id = 'firmware-uploader-dialog-container'; super();
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
} }
@postConstruct() @postConstruct()
@@ -61,13 +59,20 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
}); });
} }
get value(): void { protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
return; this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
} }
protected override render(): React.ReactNode { protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return ( return (
<div>
<form> <form>
<FirmwareUploaderComponent <FirmwareUploaderComponent
availableBoards={this.availableBoards} availableBoards={this.availableBoards}
@@ -77,18 +82,56 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
isOpen={this.isOpen} isOpen={this.isOpen}
/> />
</form> </form>
</div>
); );
} }
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private busy = false;
constructor(
@inject(UploadFirmwareDialogProps)
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected override onAfterAttach(msg: Message): void { protected override onAfterAttach(msg: Message): void {
const firstButton = this.node.querySelector('button'); if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus(); firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg); super.onAfterAttach(msg);
this.update(); this.update();
} }
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override handleEnter(event: KeyboardEvent): boolean | void { protected override handleEnter(event: KeyboardEvent): boolean | void {
return false; return false;
} }
@@ -97,11 +140,11 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
if (this.busy) { if (this.busy) {
return; return;
} }
this.widget.close();
super.close(); super.close();
this.isOpen = new Object();
} }
private busyCallback(busy: boolean): void { busyCallback(busy: boolean): void {
this.busy = busy; this.busy = busy;
if (busy) { if (busy) {
this.closeCrossNode.classList.add('disabled'); this.closeCrossNode.classList.add('disabled');
@@ -109,11 +152,4 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
this.closeCrossNode.classList.remove('disabled'); this.closeCrossNode.classList.remove('disabled');
} }
} }
private flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
} }

View File

@@ -1,6 +1,7 @@
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { shell } from '@theia/core/electron-shared/@electron/remote'; import { shell } from 'electron';
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown 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';
@@ -27,19 +28,32 @@ export const IDEUpdaterComponent = ({
}, },
}: IDEUpdaterComponentProps): React.ReactElement => { }: IDEUpdaterComponentProps): React.ReactElement => {
const { version, releaseNotes } = updateInfo; const { version, releaseNotes } = updateInfo;
const [changelog, setChangelog] = React.useState<string>(''); const changelogDivRef =
React.useRef() as React.MutableRefObject<HTMLDivElement>;
React.useEffect(() => { React.useEffect(() => {
if (releaseNotes) { if (!!releaseNotes && changelogDivRef.current) {
setChangelog( let changelog: string;
typeof releaseNotes === 'string' if (typeof releaseNotes === 'string') changelog = releaseNotes;
? releaseNotes else
: releaseNotes.reduce( changelog = releaseNotes.reduce((acc, item) => {
(acc, item) => (item.note ? (acc += `${item.note}\n\n`) : acc), return item.note ? (acc += `${item.note}\n\n`) : acc;
'' }, '');
) ReactDOM.render(
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a onClick={() => href && shell.openExternal(href)} {...props}>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>,
changelogDivRef.current
); );
} }
}, [releaseNotes, changelog]); }, [updateInfo]);
const DownloadCompleted: () => React.ReactElement = () => ( const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded"> <div className="ide-updater-dialog--downloaded">
@@ -92,24 +106,9 @@ export const IDEUpdaterComponent = ({
version version
)} )}
</div> </div>
{changelog && ( {releaseNotes && (
<div className="dialogRow changelog-container"> <div className="dialogRow changelog-container">
<div className="changelog"> <div className="changelog" ref={changelogDivRef} />
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a
onClick={() => href && shell.openExternal(href)}
{...props}
>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -5,8 +5,10 @@ import {
postConstruct, postConstruct,
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs'; import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging'; import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactDialog } from '../../theia/dialogs/dialogs'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { nls } from '@theia/core'; import { nls } from '@theia/core';
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component'; import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
import { import {
@@ -20,11 +22,47 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software'; const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {
private _updateInfo: UpdateInfo;
private _updateProgress: UpdateProgress = {};
setUpdateInfo(updateInfo: UpdateInfo): void {
this._updateInfo = updateInfo;
this.update();
}
mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
get updateInfo(): UpdateInfo {
return this._updateInfo;
}
get updateProgress(): UpdateProgress {
return this._updateProgress;
}
protected render(): React.ReactNode {
return !!this._updateInfo ? (
<IDEUpdaterComponent
updateInfo={this._updateInfo}
updateProgress={this._updateProgress}
/>
) : null;
}
}
@injectable() @injectable()
export class IDEUpdaterDialogProps extends DialogProps {} export class IDEUpdaterDialogProps extends DialogProps {}
@injectable() @injectable()
export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> { export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(IDEUpdaterDialogWidget)
private readonly widget: IDEUpdaterDialogWidget;
@inject(IDEUpdater) @inject(IDEUpdater)
private readonly updater: IDEUpdater; private readonly updater: IDEUpdater;
@@ -37,9 +75,6 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
@inject(WindowService) @inject(WindowService)
private readonly windowService: WindowService; private readonly windowService: WindowService;
private _updateInfo: UpdateInfo | undefined;
private _updateProgress: UpdateProgress = {};
constructor( constructor(
@inject(IDEUpdaterDialogProps) @inject(IDEUpdaterDialogProps)
protected override readonly props: IDEUpdaterDialogProps protected override readonly props: IDEUpdaterDialogProps
@@ -59,34 +94,26 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
protected init(): void { protected init(): void {
this.updaterClient.onUpdaterDidFail((error) => { this.updaterClient.onUpdaterDidFail((error) => {
this.appendErrorButtons(); this.appendErrorButtons();
this.mergeUpdateProgress({ error }); this.widget.mergeUpdateProgress({ error });
}); });
this.updaterClient.onDownloadProgressDidChange((progressInfo) => { this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
this.mergeUpdateProgress({ progressInfo }); this.widget.mergeUpdateProgress({ progressInfo });
}); });
this.updaterClient.onDownloadDidFinish(() => { this.updaterClient.onDownloadDidFinish(() => {
this.appendInstallButtons(); this.appendInstallButtons();
this.mergeUpdateProgress({ downloadFinished: true }); this.widget.mergeUpdateProgress({ downloadFinished: true });
}); });
} }
protected render(): React.ReactNode { get value(): UpdateInfo {
return ( return this.widget.updateInfo;
this.updateInfo && (
<IDEUpdaterComponent
updateInfo={this.updateInfo}
updateProgress={this.updateProgress}
/>
)
);
}
get value(): UpdateInfo | undefined {
return this.updateInfo;
} }
protected override onAfterAttach(msg: Message): void { protected override onAfterAttach(msg: Message): void {
this.update(); if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.appendInitialButtons(); this.appendInitialButtons();
super.onAfterAttach(msg); super.onAfterAttach(msg);
} }
@@ -169,19 +196,15 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
} }
private skipVersion(): void { private skipVersion(): void {
if (!this.updateInfo) {
console.warn(`Nothing to skip. No update info is available`);
return;
}
this.localStorageService.setData<string>( this.localStorageService.setData<string>(
SKIP_IDE_VERSION, SKIP_IDE_VERSION,
this.updateInfo.version this.widget.updateInfo.version
); );
this.close(); this.close();
} }
private startDownload(): void { private startDownload(): void {
this.mergeUpdateProgress({ this.widget.mergeUpdateProgress({
downloadStarted: true, downloadStarted: true,
}); });
this.clearButtons(); this.clearButtons();
@@ -193,48 +216,31 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
this.close(); this.close();
} }
private set updateInfo(updateInfo: UpdateInfo | undefined) {
this._updateInfo = updateInfo;
this.update();
}
private get updateInfo(): UpdateInfo | undefined {
return this._updateInfo;
}
private get updateProgress(): UpdateProgress {
return this._updateProgress;
}
private mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
override async open( override async open(
data: UpdateInfo | undefined = undefined data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> { ): Promise<UpdateInfo | undefined> {
if (data && data.version) { if (data && data.version) {
this.mergeUpdateProgress({ this.widget.mergeUpdateProgress({
progressInfo: undefined, progressInfo: undefined,
downloadStarted: false, downloadStarted: false,
downloadFinished: false, downloadFinished: false,
error: undefined, error: undefined,
}); });
this.updateInfo = data; this.widget.setUpdateInfo(data);
return super.open(); return super.open();
} }
} }
protected override onActivateRequest(msg: Message): void { protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg); super.onActivateRequest(msg);
this.update(); this.widget.activate();
} }
override close(): void { override close(): void {
this.widget.dispose();
if ( if (
this.updateProgress?.downloadStarted && this.widget.updateProgress?.downloadStarted &&
!this.updateProgress?.downloadFinished !this.widget.updateProgress?.downloadFinished
) { ) {
this.updater.stopDownload(); this.updater.stopDownload();
} }

View File

@@ -218,10 +218,12 @@ export class SettingsComponent extends React.Component<
<div className="flex-line"> <div className="flex-line">
<select <select
className="theia-select" className="theia-select"
value={this.props.themeService.getCurrentTheme().label} value={ThemeService.get().getCurrentTheme().label}
onChange={this.themeDidChange} onChange={this.themeDidChange}
> >
{this.props.themeService.getThemes().map(({ id, label }) => ( {ThemeService.get()
.getThemes()
.map(({ id, label }) => (
<option key={id} value={label}> <option key={id} value={label}>
{label} {label}
</option> </option>
@@ -406,7 +408,7 @@ export class SettingsComponent extends React.Component<
} }
onChange={this.socksProtocolDidChange} onChange={this.socksProtocolDidChange}
/> />
SOCKS5 SOCKS
</label> </label>
</form> </form>
<div className="flex-line proxy-settings"> <div className="flex-line proxy-settings">
@@ -610,11 +612,11 @@ export class SettingsComponent extends React.Component<
event: React.ChangeEvent<HTMLSelectElement> event: React.ChangeEvent<HTMLSelectElement>
): void => { ): void => {
const { selectedIndex } = event.target.options; const { selectedIndex } = event.target.options;
const theme = this.props.themeService.getThemes()[selectedIndex]; const theme = ThemeService.get().getThemes()[selectedIndex];
if (theme) { if (theme) {
this.setState({ themeId: theme.id }); this.setState({ themeId: theme.id });
if (this.props.themeService.getCurrentTheme().id !== theme.id) { if (ThemeService.get().getCurrentTheme().id !== theme.id) {
this.props.themeService.setCurrentTheme(theme.id); ThemeService.get().setCurrentTheme(theme.id);
} }
} }
}; };
@@ -682,7 +684,7 @@ export class SettingsComponent extends React.Component<
): void => { ): void => {
if (this.state.network !== 'none') { if (this.state.network !== 'none') {
const network = this.cloneProxySettings; const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks5'; network.protocol = event.target.checked ? 'http' : 'socks';
this.setState({ network }); this.setState({ network });
} }
}; };
@@ -692,7 +694,7 @@ export class SettingsComponent extends React.Component<
): void => { ): void => {
if (this.state.network !== 'none') { if (this.state.network !== 'none') {
const network = this.cloneProxySettings; const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks5' : 'http'; network.protocol = event.target.checked ? 'socks' : 'http';
this.setState({ network }); this.setState({ network });
} }
}; };
@@ -753,7 +755,6 @@ export namespace SettingsComponent {
readonly fileDialogService: FileDialogService; readonly fileDialogService: FileDialogService;
readonly windowService: WindowService; readonly windowService: WindowService;
readonly localizationProvider: AsyncLocalizationProvider; readonly localizationProvider: AsyncLocalizationProvider;
readonly themeService: ThemeService;
} }
export type State = Settings & { export type State = Settings & {
rawAdditionalUrlsValue: string; rawAdditionalUrlsValue: string;

View File

@@ -35,9 +35,6 @@ export class SettingsWidget extends ReactWidget {
@inject(AsyncLocalizationProvider) @inject(AsyncLocalizationProvider)
protected readonly localizationProvider: AsyncLocalizationProvider; protected readonly localizationProvider: AsyncLocalizationProvider;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected render(): React.ReactNode { protected render(): React.ReactNode {
return ( return (
<SettingsComponent <SettingsComponent
@@ -46,7 +43,6 @@ export class SettingsWidget extends ReactWidget {
fileDialogService={this.fileDialogService} fileDialogService={this.fileDialogService}
windowService={this.windowService} windowService={this.windowService}
localizationProvider={this.localizationProvider} localizationProvider={this.localizationProvider}
themeService={this.themeService}
/> />
); );
} }
@@ -63,9 +59,6 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsWidget) @inject(SettingsWidget)
protected readonly widget: SettingsWidget; protected readonly widget: SettingsWidget;
@inject(ThemeService)
private readonly themeService: ThemeService;
constructor( constructor(
@inject(SettingsDialogProps) @inject(SettingsDialogProps)
protected override readonly props: SettingsDialogProps protected override readonly props: SettingsDialogProps
@@ -128,11 +121,11 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
} }
override async open(): Promise<Promise<Settings> | undefined> { override async open(): Promise<Promise<Settings> | undefined> {
const themeIdBeforeOpen = this.themeService.getCurrentTheme().id; const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id;
const result = await super.open(); const result = await super.open();
if (!result) { if (!result) {
if (this.themeService.getCurrentTheme().id !== themeIdBeforeOpen) { if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) {
this.themeService.setCurrentTheme(themeIdBeforeOpen); ThemeService.get().setCurrentTheme(themeIdBeforeOpen);
} }
} }
return result; return result;

View File

@@ -5,7 +5,7 @@ import {
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects'; import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeService } from '@theia/core/lib/browser/theming';
@@ -25,21 +25,17 @@ import {
LanguageInfo, LanguageInfo,
} from '@theia/core/lib/common/i18n/localization'; } from '@theia/core/lib/common/i18n/localization';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'; import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { DefaultTheme } from '@theia/application-package/lib/application-props';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import type { FileStat } from '@theia/filesystem/lib/common/files';
export const WINDOW_SETTING = 'window';
export const EDITOR_SETTING = 'editor'; export const EDITOR_SETTING = 'editor';
export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`; export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`;
export const AUTO_SAVE_SETTING = `files.autoSave`; export const AUTO_SAVE_SETTING = `files.autoSave`;
export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`; export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`;
export const ARDUINO_SETTING = 'arduino'; export const ARDUINO_SETTING = 'arduino';
export const ARDUINO_WINDOW_SETTING = `${ARDUINO_SETTING}.window`; export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`; export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`;
export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`; export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`;
export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`; export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`;
export const AUTO_SCALE_SETTING = `${ARDUINO_WINDOW_SETTING}.autoScale`; export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`;
export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`; export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`;
export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`; export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`;
export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`; export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`;
@@ -57,7 +53,7 @@ export interface Settings {
currentLanguage: string; currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale` autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `window.zoomLevel` interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
verboseOnCompile: boolean; // `arduino.compile.verbose` verboseOnCompile: boolean; // `arduino.compile.verbose`
compilerWarnings: CompilerWarnings; // `arduino.compile.warnings` compilerWarnings: CompilerWarnings; // `arduino.compile.warnings`
verboseOnUpload: boolean; // `arduino.upload.verbose` verboseOnUpload: boolean; // `arduino.upload.verbose`
@@ -105,9 +101,6 @@ export class SettingsService {
@inject(CommandService) @inject(CommandService)
protected commandService: CommandService; protected commandService: CommandService;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>(); protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event; readonly onDidChange = this.onDidChangeEmitter.event;
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>(); protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
@@ -148,9 +141,10 @@ export class SettingsService {
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12), this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
this.preferenceService.get<string>( this.preferenceService.get<string>(
'workbench.colorTheme', 'workbench.colorTheme',
DefaultTheme.defaultForOSTheme( window.matchMedia &&
FrontendApplicationConfigProvider.get().defaultTheme window.matchMedia('(prefers-color-scheme: dark)').matches
) ? 'arduino-theme-dark'
: 'arduino-theme'
), ),
this.preferenceService.get<Settings.AutoSave>( this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING, AUTO_SAVE_SETTING,
@@ -172,15 +166,7 @@ export class SettingsService {
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false), this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
this.configService.getConfiguration(), this.configService.getConfiguration(),
]); ]);
const { const { additionalUrls, sketchDirUri, network } = cliConfig;
config = {
additionalUrls: [],
sketchDirUri: '',
network: Network.Default(),
},
} = cliConfig;
const { additionalUrls, sketchDirUri, network } = config;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri)); const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return { return {
editorFontSize, editorFontSize,
@@ -232,11 +218,7 @@ export class SettingsService {
try { try {
const { sketchbookPath, editorFontSize, themeId } = await settings; const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath); const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
let sketchbookStat: FileStat | undefined = undefined; if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
try {
sketchbookStat = await this.fileService.resolve(new URI(sketchbookDir));
} catch {}
if (!sketchbookStat || !sketchbookStat.isDirectory) {
return nls.localize( return nls.localize(
'arduino/preferences/invalid.sketchbook.location', 'arduino/preferences/invalid.sketchbook.location',
'Invalid sketchbook location: {0}', 'Invalid sketchbook location: {0}',
@@ -249,7 +231,11 @@ export class SettingsService {
'Invalid editor font size. It must be a positive integer.' 'Invalid editor font size. It must be a positive integer.'
); );
} }
if (!this.themeService.getThemes().find(({ id }) => id === themeId)) { if (
!ThemeService.get()
.getThemes()
.find(({ id }) => id === themeId)
) {
return nls.localize( return nls.localize(
'arduino/preferences/invalid.theme', 'arduino/preferences/invalid.theme',
'Invalid theme.' 'Invalid theme.'
@@ -266,6 +252,7 @@ export class SettingsService {
private async savePreference(name: string, value: unknown): Promise<void> { private async savePreference(name: string, value: unknown): Promise<void> {
await this.preferenceService.set(name, value, PreferenceScope.User); await this.preferenceService.set(name, value, PreferenceScope.User);
await timeout(5);
} }
async save(): Promise<string | true> { async save(): Promise<string | true> {
@@ -287,38 +274,28 @@ export class SettingsService {
network, network,
sketchbookShowAllFiles, sketchbookShowAllFiles,
} = this._settings; } = this._settings;
const [cliConfig, sketchDirUri] = await Promise.all([ const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(), this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath), this.fileSystemExt.getUri(sketchbookPath),
]); ]);
const { config } = cliConfig;
if (!config) {
// Do not check for any error messages. The config might has errors (such as invalid directories.user) right before saving the new values.
return nls.localize(
'arduino/preferences/noCliConfig',
'Could not load the CLI configuration'
);
}
(config as any).additionalUrls = additionalUrls; (config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri; (config as any).sketchDirUri = sketchDirUri;
(config as any).network = network; (config as any).network = network;
(config as any).locale = currentLanguage; (config as any).locale = currentLanguage;
await Promise.all([ await this.savePreference('editor.fontSize', editorFontSize);
this.savePreference('editor.fontSize', editorFontSize), await this.savePreference('workbench.colorTheme', themeId);
this.savePreference('workbench.colorTheme', themeId), await this.savePreference(AUTO_SAVE_SETTING, autoSave);
this.savePreference(AUTO_SAVE_SETTING, autoSave), await this.savePreference('editor.quickSuggestions', quickSuggestions);
this.savePreference('editor.quickSuggestions', quickSuggestions), await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface);
this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface), await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale), await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile), await this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile);
this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings), await this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings);
this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload), await this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload);
this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload), await this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload);
this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles), await this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles);
this.configService.setConfiguration(config), await this.configService.setConfiguration(config);
]);
this.onDidChangeEmitter.fire(this._settings); this.onDidChangeEmitter.fire(this._settings);
// after saving all the settings, if we need to change the language we need to perform a reload // after saving all the settings, if we need to change the language we need to perform a reload

View File

@@ -1,18 +1,63 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs'; import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging'; import { Message } from '@theia/core/shared/@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch'; import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component'; import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol'; import { BoardUserField } from '../../../common/protocol';
import { ReactDialog } from '../../theia/dialogs/dialogs';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
private _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
}
set currentUserFields(userFields: BoardUserField[]) {
this.setUserFields(userFields);
}
get currentUserFields(): BoardUserField[] {
return this._currentUserFields;
}
resetUserFieldsValue(): void {
this._currentUserFields = this._currentUserFields.map((field) => {
field.value = '';
return field;
});
}
private setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}
protected render(): React.ReactNode {
return (
<form>
<UserFieldsComponent
initialBoardUserFields={this._currentUserFields}
updateUserFields={this.setUserFields.bind(this)}
cancel={this.cancel}
accept={this.accept}
/>
</form>
);
}
}
@injectable() @injectable()
export class UserFieldsDialogProps extends DialogProps {} export class UserFieldsDialogProps extends DialogProps {}
@injectable() @injectable()
export class UserFieldsDialog extends ReactDialog<BoardUserField[]> { export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
private _currentUserFields: BoardUserField[] = []; protected readonly widget: UserFieldsDialogWidget;
constructor( constructor(
@inject(UserFieldsDialogProps) @inject(UserFieldsDialogProps)
@@ -24,36 +69,39 @@ export class UserFieldsDialog extends ReactDialog<BoardUserField[]> {
this.titleNode.classList.add('user-fields-dialog-title'); this.titleNode.classList.add('user-fields-dialog-title');
this.contentNode.classList.add('user-fields-dialog-content'); this.contentNode.classList.add('user-fields-dialog-content');
this.acceptButton = undefined; this.acceptButton = undefined;
} this.widget = new UserFieldsDialogWidget(
this.close.bind(this),
get value(): BoardUserField[] { this.accept.bind(this)
return this._currentUserFields;
}
set value(userFields: BoardUserField[]) {
this._currentUserFields = userFields;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<UserFieldsComponent
initialBoardUserFields={this.value}
updateUserFields={this.doUpdateUserFields}
cancel={this.doCancel}
accept={this.doAccept}
/>
</form>
</div>
); );
} }
set value(userFields: BoardUserField[]) {
this.widget.currentUserFields = userFields;
}
get value(): BoardUserField[] {
return this.widget.currentUserFields;
}
protected override onAfterAttach(msg: Message): void { protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg); super.onAfterAttach(msg);
this.update(); this.update();
} }
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override async accept(): Promise<void> { protected override async accept(): Promise<void> {
// If the user presses enter and at least // If the user presses enter and at least
// a field is empty don't accept the input // a field is empty don't accept the input
@@ -66,21 +114,8 @@ export class UserFieldsDialog extends ReactDialog<BoardUserField[]> {
} }
override close(): void { override close(): void {
this.resetUserFieldsValue(); this.widget.resetUserFieldsValue();
this.widget.close();
super.close(); super.close();
} }
private resetUserFieldsValue(): void {
this.value = this.value.map((field) => {
field.value = '';
return field;
});
}
private readonly doCancel: () => void = () => this.close();
private readonly doAccept: () => Promise<void> = () => this.accept();
private readonly doUpdateUserFields: (userFields: BoardUserField[]) => void =
(userFields: BoardUserField[]) => {
this.value = userFields;
};
} }

View File

@@ -1,6 +0,0 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/dark/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#C5C5C5" />
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,6 +0,0 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/light/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#424242" />
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -41,6 +41,7 @@ export class LibraryListWidget extends ListWidget<
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: LibraryPackage) => item.name, itemLabel: (item: LibraryPackage) => item.name,
itemDeprecated: (item: LibraryPackage) => item.deprecated,
itemRenderer, itemRenderer,
filterRenderer, filterRenderer,
defaultSearchOptions: { query: '', type: 'All', topic: 'All' }, defaultSearchOptions: { query: '', type: 'All', topic: 'All' },

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from '@theia/core/shared/vscode-uri'; import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { import {
@@ -88,25 +88,8 @@ export class LocalCacheFsProvider
} }
protected async init(fileService: FileService): Promise<void> { protected async init(fileService: FileService): Promise<void> {
const { config } = await this.configService.getConfiguration(); const config = await this.configService.getConfiguration();
// Any possible CLI config errors are ignored here. IDE2 does not verify the `directories.data` folder. this._localCacheRoot = new URI(config.dataDirUri);
// If the data dir is accessible, IDE2 creates the cache folder for the cloud sketches. Otherwise, it does not.
// The data folder can be configured outside of the IDE2, and the new data folder will be picked up with a
// subsequent IDE2 start.
if (!config?.dataDirUri) {
return; // the deferred promise will never resolve
}
const localCacheUri = new URI(config.dataDirUri);
try {
await fileService.access(localCacheUri);
} catch (err) {
console.error(
`'directories.data' location is inaccessible at ${config.dataDirUri}`,
err
);
return;
}
this._localCacheRoot = localCacheUri;
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) { for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment); this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot); await fileService.createFolder(this._localCacheRoot);

View File

@@ -97,11 +97,6 @@ export namespace ArduinoMenus {
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection']; export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader` // Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings']; export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// `Tool` > `Ports` (always visible https://github.com/arduino/arduino-ide/issues/655)
export const TOOLS__PORTS_SUBMENU = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
// -- Help // -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc. // `Getting Started`, `Environment`, `Troubleshooting`, etc.

View File

@@ -46,6 +46,7 @@ export class MonitorManagerProxyClientImpl
private wsPort?: number; private wsPort?: number;
private lastConnectedBoard: BoardsConfig.Config; private lastConnectedBoard: BoardsConfig.Config;
private onBoardsConfigChanged: Disposable | undefined; private onBoardsConfigChanged: Disposable | undefined;
private isMonitorWidgetOpen = false;
getWebSocketPort(): number | undefined { getWebSocketPort(): number | undefined {
return this.wsPort; return this.wsPort;
@@ -174,6 +175,14 @@ export class MonitorManagerProxyClientImpl
return this.server().getCurrentSettings(board, port); return this.server().getCurrentSettings(board, port);
} }
setMonitorWidgetStatus(value: boolean): void {
this.isMonitorWidgetOpen = value;
}
getMonitorWidgetStatus(): boolean {
return this.isMonitorWidgetOpen;
}
send(message: string): void { send(message: string): void {
if (!this.webSocket) { if (!this.webSocket) {
return; return;

View File

@@ -18,7 +18,7 @@ import {
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
BoardsPackage, BoardsPackage,
LibraryPackage, LibraryPackage,
ConfigState, Config,
Sketch, Sketch,
ProgressMessage, ProgressMessage,
} from '../common/protocol'; } from '../common/protocol';
@@ -37,7 +37,6 @@ export class NotificationCenter
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService; private readonly appStateService: FrontendApplicationStateService;
private readonly didReinitializeEmitter = new Emitter<void>();
private readonly indexUpdateDidCompleteEmitter = private readonly indexUpdateDidCompleteEmitter =
new Emitter<IndexUpdateDidCompleteParams>(); new Emitter<IndexUpdateDidCompleteParams>();
private readonly indexUpdateWillStartEmitter = private readonly indexUpdateWillStartEmitter =
@@ -48,7 +47,9 @@ export class NotificationCenter
new Emitter<IndexUpdateDidFailParams>(); new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<string>(); private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStopEmitter = new Emitter<void>(); private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<ConfigState>(); private readonly configDidChangeEmitter = new Emitter<{
config: Config | undefined;
}>();
private readonly platformDidInstallEmitter = new Emitter<{ private readonly platformDidInstallEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
@@ -56,7 +57,7 @@ export class NotificationCenter
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
private readonly libraryDidInstallEmitter = new Emitter<{ private readonly libraryDidInstallEmitter = new Emitter<{
item: LibraryPackage | 'zip-install'; item: LibraryPackage;
}>(); }>();
private readonly libraryDidUninstallEmitter = new Emitter<{ private readonly libraryDidUninstallEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
@@ -70,7 +71,6 @@ export class NotificationCenter
new Emitter<FrontendApplicationState>(); new Emitter<FrontendApplicationState>();
private readonly toDispose = new DisposableCollection( private readonly toDispose = new DisposableCollection(
this.didReinitializeEmitter,
this.indexUpdateWillStartEmitter, this.indexUpdateWillStartEmitter,
this.indexUpdateDidProgressEmitter, this.indexUpdateDidProgressEmitter,
this.indexUpdateDidCompleteEmitter, this.indexUpdateDidCompleteEmitter,
@@ -85,7 +85,6 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter this.attachedBoardsDidChangeEmitter
); );
readonly onDidReinitialize = this.didReinitializeEmitter.event;
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event; readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event; readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event; readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
@@ -116,10 +115,6 @@ export class NotificationCenter
this.toDispose.dispose(); this.toDispose.dispose();
} }
notifyDidReinitialize(): void {
this.didReinitializeEmitter.fire();
}
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void { notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.indexUpdateWillStartEmitter.fire(params); this.indexUpdateWillStartEmitter.fire(params);
} }
@@ -144,7 +139,7 @@ export class NotificationCenter
this.daemonDidStopEmitter.fire(); this.daemonDidStopEmitter.fire();
} }
notifyConfigDidChange(event: ConfigState): void { notifyConfigDidChange(event: { config: Config | undefined }): void {
this.configDidChangeEmitter.fire(event); this.configDidChangeEmitter.fire(event);
} }
@@ -156,9 +151,7 @@ export class NotificationCenter
this.platformDidUninstallEmitter.fire(event); this.platformDidUninstallEmitter.fire(event);
} }
notifyLibraryDidInstall(event: { notifyLibraryDidInstall(event: { item: LibraryPackage }): void {
item: LibraryPackage | 'zip-install';
}): void {
this.libraryDidInstallEmitter.fire(event); this.libraryDidInstallEmitter.fire(event);
} }

View File

@@ -1,5 +1,6 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify'; import { injectable, inject } from '@theia/core/shared/inversify';
import { OptionsType } from 'react-select/src/types';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable'; import { Disposable } from '@theia/core/lib/common/disposable';
import { import {
@@ -73,6 +74,10 @@ export class MonitorWidget extends ReactWidget {
this.monitorManagerProxy.startMonitor(); this.monitorManagerProxy.startMonitor();
} }
protected override onAfterAttach(msg: Message): void {
this.monitorManagerProxy.setMonitorWidgetStatus(this.isAttached);
}
onMonitorSettingsDidChange(settings: MonitorSettings): void { onMonitorSettingsDidChange(settings: MonitorSettings): void {
this.settings = { this.settings = {
...this.settings, ...this.settings,
@@ -90,6 +95,7 @@ export class MonitorWidget extends ReactWidget {
} }
override dispose(): void { override dispose(): void {
this.monitorManagerProxy.setMonitorWidgetStatus(this.isAttached);
super.dispose(); super.dispose();
} }
@@ -127,7 +133,9 @@ export class MonitorWidget extends ReactWidget {
); );
}; };
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorModel.EOL>[] { protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
return [ return [
{ {
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'), label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),

View File

@@ -65,6 +65,9 @@ export class PlotterFrontendContribution extends Contribution {
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => { ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
if (!!this.window) { if (!!this.window) {
if (!this.monitorManagerProxy.getMonitorWidgetStatus()) {
this.monitorManagerProxy.disconnect();
}
this.window = null; this.window = null;
} }
}); });

View File

@@ -1,293 +0,0 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Sketch, SketchesService } from '../common/protocol';
import { ConfigServiceClient } from './config/config-service-client';
import {
SketchContainer,
SketchesError,
SketchRef,
} from '../common/protocol/sketches-service';
import {
ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER,
} from './utils/constants';
import * as monaco from '@theia/monaco-editor-core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
const READ_ONLY_FILES = ['sketch.json'];
const READ_ONLY_FILES_REMOTE = ['thingProperties.h', 'thingsProperties.h'];
export type CurrentSketch = Sketch | 'invalid';
export namespace CurrentSketch {
export function isValid(arg: CurrentSketch | undefined): arg is Sketch {
return !!arg && arg !== 'invalid';
}
}
@injectable()
export class SketchesServiceClientImpl
implements FrontendApplicationContribution
{
@inject(FileService)
private readonly fileService: FileService;
@inject(SketchesService)
private readonly sketchesService: SketchesService;
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;
@inject(ConfigServiceClient)
private readonly configService: ConfigServiceClient;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private sketches = new Map<string, SketchRef>();
private onSketchbookDidChangeEmitter = new Emitter<{
created: SketchRef[];
removed: SketchRef[];
}>();
readonly onSketchbookDidChange = this.onSketchbookDidChangeEmitter.event;
private currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
private toDisposeBeforeWatchSketchbookDir = new DisposableCollection();
private toDispose = new DisposableCollection(
this.onSketchbookDidChangeEmitter,
this.currentSketchDidChangeEmitter,
this.toDisposeBeforeWatchSketchbookDir
);
private _currentSketch: CurrentSketch | undefined;
private currentSketchLoaded = new Deferred<CurrentSketch>();
onStart(): void {
const sketchDirUri = this.configService.tryGetSketchDirUri();
this.watchSketchbookDir(sketchDirUri);
const refreshCurrentSketch = async () => {
const currentSketch = await this.loadCurrentSketch();
this.useCurrentSketch(currentSketch);
};
this.toDispose.push(
this.configService.onDidChangeSketchDirUri((sketchDirUri) => {
this.watchSketchbookDir(sketchDirUri);
refreshCurrentSketch();
})
);
this.appStateService
.reachedState('started_contributions')
.then(refreshCurrentSketch);
}
private async watchSketchbookDir(
sketchDirUri: URI | undefined
): Promise<void> {
this.toDisposeBeforeWatchSketchbookDir.dispose();
if (!sketchDirUri) {
return;
}
const container = await this.sketchesService.getSketches({
uri: sketchDirUri.toString(),
});
for (const sketch of SketchContainer.toArray(container)) {
this.sketches.set(sketch.uri, sketch);
}
this.toDisposeBeforeWatchSketchbookDir.pushAll([
Disposable.create(() => this.sketches.clear()),
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
this.fileService.watch(sketchDirUri, {
recursive: true,
excludes: [],
}),
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
// The file change events have higher precedence in the current sketch over the sketchbook.
if (
CurrentSketch.isValid(this._currentSketch) &&
new URI(this._currentSketch.uri).isEqualOrParent(resource)
) {
// https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656
// On a sketch file rename, the FS watcher will contain two changes:
// - Deletion of the original file,
// - Update of the new file,
// Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event.
// Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2.
if (type === FileChangeType.UPDATED && event.changes.length === 1) {
// If the event contains only one `UPDATE` change, it cannot be a rename.
return;
}
let reloadedSketch: Sketch | undefined = undefined;
try {
reloadedSketch = await this.sketchesService.loadSketch(
this._currentSketch.uri
);
} catch (err) {
if (!SketchesError.NotFound.is(err)) {
throw err;
}
}
if (!reloadedSketch) {
return;
}
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
this.useCurrentSketch(reloadedSketch, true);
}
return;
}
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
if (sketchDirUri.isEqualOrParent(resource)) {
if (Sketch.isSketchFile(resource)) {
if (type === FileChangeType.ADDED) {
try {
const toAdd = await this.sketchesService.loadSketch(
resource.parent.toString()
);
if (!this.sketches.has(toAdd.uri)) {
console.log(
`New sketch '${toAdd.name}' was created in sketchbook '${sketchDirUri}'.`
);
this.sketches.set(toAdd.uri, toAdd);
this.fireSoon(toAdd, 'created');
}
} catch {}
} else if (type === FileChangeType.DELETED) {
const uri = resource.parent.toString();
const toDelete = this.sketches.get(uri);
if (toDelete) {
console.log(
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchDirUri}'.`
);
this.sketches.delete(uri);
this.fireSoon(toDelete, 'removed');
}
}
}
}
}
}),
]);
}
private useCurrentSketch(
currentSketch: CurrentSketch,
reassignPromise = false
) {
this._currentSketch = currentSketch;
if (reassignPromise) {
this.currentSketchLoaded = new Deferred();
}
this.currentSketchLoaded.resolve(this._currentSketch);
this.currentSketchDidChangeEmitter.fire(this._currentSketch);
}
onStop(): void {
this.toDispose.dispose();
}
private async loadCurrentSketch(): Promise<CurrentSketch> {
const sketches = (
await Promise.all(
this.workspaceService
.tryGetRoots()
.map(({ resource }) =>
this.sketchesService.getSketchFolder(resource.toString())
)
)
).filter(notEmpty);
if (!sketches.length) {
return 'invalid';
}
if (sketches.length > 1) {
console.log(
`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(
sketches
)}`
);
}
return sketches[0];
}
async currentSketch(): Promise<CurrentSketch> {
return this.currentSketchLoaded.promise;
}
tryGetCurrentSketch(): CurrentSketch | undefined {
return this._currentSketch;
}
async currentSketchFile(): Promise<string | undefined> {
const currentSketch = await this.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
return currentSketch.mainFileUri;
}
return undefined;
}
private fireSoonHandle?: number;
private bufferedSketchbookEvents: {
type: 'created' | 'removed';
sketch: SketchRef;
}[] = [];
private fireSoon(sketch: SketchRef, type: 'created' | 'removed'): void {
this.bufferedSketchbookEvents.push({ type, sketch });
if (typeof this.fireSoonHandle === 'number') {
window.clearTimeout(this.fireSoonHandle);
}
this.fireSoonHandle = window.setTimeout(() => {
const event: { created: SketchRef[]; removed: SketchRef[] } = {
created: [],
removed: [],
};
for (const { type, sketch } of this.bufferedSketchbookEvents) {
if (type === 'created') {
event.created.push(sketch);
} else {
event.removed.push(sketch);
}
}
this.onSketchbookDidChangeEmitter.fire(event);
this.bufferedSketchbookEvents.length = 0;
}, 100);
}
/**
* `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`.
*/
isReadOnly(uri: URI | monaco.Uri | string): boolean {
const toCheck = uri instanceof URI ? uri : new URI(uri);
if (toCheck.scheme === 'user-storage') {
return false;
}
const isCloudSketch = toCheck
.toString()
.includes(`${REMOTE_SKETCHBOOK_FOLDER}/${ARDUINO_CLOUD_FOLDER}`);
const filesToCheck = [
...READ_ONLY_FILES,
...(isCloudSketch ? READ_ONLY_FILES_REMOTE : []),
];
if (filesToCheck.includes(toCheck?.path?.base)) {
return true;
}
const readOnly = !this.workspaceService
.tryGetRoots()
.some(({ resource }) => resource.isEqualOrParent(toCheck));
return readOnly;
}
}

View File

@@ -20,16 +20,6 @@
@import './progress-bar.css'; @import './progress-bar.css';
@import './settings-step-input.css'; @import './settings-step-input.css';
/* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */
/* The SVG icons are still part of Theia (1.31.1) */
/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */
body {
--theia-icon-loading: url(../icons/loading-light.svg);
}
body.theia-dark {
--theia-icon-loading: url(../icons/loading-dark.svg);
}
.theia-input.warning:focus { .theia-input.warning:focus {
outline-width: 1px; outline-width: 1px;
outline-style: solid; outline-style: solid;
@@ -176,13 +166,3 @@ button.theia-button.message-box-dialog-button {
outline: 1px dashed var(--theia-focusBorder); outline: 1px dashed var(--theia-focusBorder);
outline-offset: -2px; outline-offset: -2px;
} }
.debug-toolbar .debug-action>div {
font-family: var(--theia-ui-font-family);
font-size: var(--theia-ui-font-size0);
display: flex;
align-items: center;
align-self: center;
justify-content: center;
min-height: inherit;
}

View File

@@ -100,19 +100,6 @@
background-color: var(--theia-titleBar-activeBackground); background-color: var(--theia-titleBar-activeBackground);
} }
#arduino-toolbar-panel {
background: var(--theia-titleBar-activeBackground);
color: var(--theia-titleBar-activeForeground);
display: flex;
min-height: var(--theia-private-menubar-height);
border-bottom: 1px solid var(--theia-titleBar-border);
}
#arduino-toolbar-panel:window-inactive,
#arduino-toolbar-panel:-moz-window-inactive {
background: var(--theia-titleBar-inactiveBackground);
color: var(--theia-titleBar-inactiveForeground);
}
#arduino-toolbar-container { #arduino-toolbar-container {
display: flex; display: flex;
width: 100%; width: 100%;

View File

@@ -7,8 +7,6 @@ import {
SHELL_TABBAR_CONTEXT_MENU, SHELL_TABBAR_CONTEXT_MENU,
TabBar, TabBar,
Widget, Widget,
Layout,
SplitPanel,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { import {
ConnectionStatus, ConnectionStatus,
@@ -19,11 +17,6 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { ToolbarAwareTabBar } from './tab-bars'; import { ToolbarAwareTabBar } from './tab-bars';
interface WidgetOptions
extends Omit<TheiaApplicationShell.WidgetOptions, 'area'> {
area?: TheiaApplicationShell.Area | 'toolbar';
}
@injectable() @injectable()
export class ApplicationShell extends TheiaApplicationShell { export class ApplicationShell extends TheiaApplicationShell {
@inject(MessageService) @inject(MessageService)
@@ -31,11 +24,10 @@ export class ApplicationShell extends TheiaApplicationShell {
@inject(ConnectionStatusService) @inject(ConnectionStatusService)
private readonly connectionStatusService: ConnectionStatusService; private readonly connectionStatusService: ConnectionStatusService;
private toolbarPanel: Panel;
override async addWidget( override async addWidget(
widget: Widget, widget: Widget,
options: Readonly<WidgetOptions> = {} options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
): Promise<void> { ): Promise<void> {
// By default, Theia open a widget **next** to the currently active in the target area. // By default, Theia open a widget **next** to the currently active in the target area.
// Instead of this logic, we want to open the new widget after the last of the target area. // Instead of this logic, we want to open the new widget after the last of the target area.
@@ -45,12 +37,8 @@ export class ApplicationShell extends TheiaApplicationShell {
); );
return; return;
} }
if (options.area === 'toolbar') {
this.toolbarPanel.addWidget(widget);
return;
}
const area = options.area || 'main';
let ref: Widget | undefined = options.ref; let ref: Widget | undefined = options.ref;
const area: TheiaApplicationShell.Area = options.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) { if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area); const tabBar = this.getTabBarFor(area);
if (tabBar) { if (tabBar) {
@@ -60,10 +48,7 @@ export class ApplicationShell extends TheiaApplicationShell {
} }
} }
} }
return super.addWidget(widget, { return super.addWidget(widget, { ...options, ref });
...(<TheiaApplicationShell.WidgetOptions>options),
ref,
});
} }
override handleEvent(): boolean { override handleEvent(): boolean {
@@ -71,46 +56,6 @@ export class ApplicationShell extends TheiaApplicationShell {
return false; return false;
} }
protected override initializeShell(): void {
this.toolbarPanel = this.createToolbarPanel();
super.initializeShell();
}
private createToolbarPanel(): Panel {
const toolbarPanel = new Panel();
toolbarPanel.id = 'arduino-toolbar-panel';
toolbarPanel.show();
return toolbarPanel;
}
protected override createLayout(): Layout {
const bottomSplitLayout = this.createSplitLayout(
[this.mainPanel, this.bottomPanel],
[1, 0],
{ orientation: 'vertical', spacing: 0 }
);
const panelForBottomArea = new SplitPanel({ layout: bottomSplitLayout });
panelForBottomArea.id = 'theia-bottom-split-panel';
const leftRightSplitLayout = this.createSplitLayout(
[
this.leftPanelHandler.container,
panelForBottomArea,
this.rightPanelHandler.container,
],
[0, 1, 0],
{ orientation: 'horizontal', spacing: 0 }
);
const panelForSideAreas = new SplitPanel({ layout: leftRightSplitLayout });
panelForSideAreas.id = 'theia-left-right-split-panel';
return this.createBoxLayout(
[this.topPanel, this.toolbarPanel, panelForSideAreas, this.statusBar],
[0, 0, 1, 0],
{ direction: 'top-to-bottom', spacing: 0 }
);
}
// Avoid hiding top panel as we use it for arduino toolbar // Avoid hiding top panel as we use it for arduino toolbar
protected override createTopPanel(): Panel { protected override createTopPanel(): Panel {
const topPanel = super.createTopPanel(); const topPanel = super.createTopPanel();

View File

@@ -6,8 +6,6 @@ import {
} from '@theia/core/lib/browser/common-frontend-contribution'; } from '@theia/core/lib/browser/common-frontend-contribution';
import { CommandRegistry } from '@theia/core/lib/common/command'; import { CommandRegistry } from '@theia/core/lib/common/command';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application'; import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { isOSX } from '@theia/core';
@injectable() @injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution { export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
@@ -24,7 +22,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.TOGGLE_MAXIMIZED, CommonCommands.TOGGLE_MAXIMIZED,
CommonCommands.PIN_TAB, CommonCommands.PIN_TAB,
CommonCommands.UNPIN_TAB, CommonCommands.UNPIN_TAB,
CommonCommands.NEW_UNTITLED_FILE, CommonCommands.NEW_FILE,
]) { ]) {
commandRegistry.unregisterCommand(command); commandRegistry.unregisterCommand(command);
} }
@@ -46,43 +44,12 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.SELECT_ICON_THEME, CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME, CommonCommands.SELECT_COLOR_THEME,
CommonCommands.ABOUT_COMMAND, CommonCommands.ABOUT_COMMAND,
CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877, CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877
CommonCommands.NEW_UNTITLED_FILE,
]) { ]) {
registry.unregisterMenuAction(command); registry.unregisterMenuAction(command);
} }
} }
override registerKeybindings(registry: KeybindingRegistry): void {
super.registerKeybindings(registry);
// Workaround for https://github.com/eclipse-theia/theia/issues/11875
if (isOSX) {
registry.unregisterKeybinding('ctrlcmd+tab');
registry.unregisterKeybinding('ctrlcmd+alt+d');
registry.unregisterKeybinding('ctrlcmd+shift+tab');
registry.unregisterKeybinding('ctrlcmd+alt+a');
registry.registerKeybindings(
{
command: CommonCommands.NEXT_TAB.id,
keybinding: 'ctrl+tab',
},
{
command: CommonCommands.NEXT_TAB.id,
keybinding: 'ctrl+alt+d',
},
{
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'ctrl+shift+tab',
},
{
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'ctrl+alt+a',
}
);
}
}
override onWillStop(): OnWillStopAction | undefined { override onWillStop(): OnWillStopAction | undefined {
// This is NOOP here. All window close and app quit requests are handled in the `Close` contribution. // This is NOOP here. All window close and app quit requests are handled in the `Close` contribution.
return undefined; return undefined;

View File

@@ -1,4 +1,5 @@
import { injectable, inject } from '@theia/core/shared/inversify'; import { injectable, inject } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CommandService } from '@theia/core/lib/common/command'; import { CommandService } from '@theia/core/lib/common/command';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
@@ -7,16 +8,17 @@ import { OpenSketchFiles } from '../../contributions/open-sketch-files';
@injectable() @injectable()
export class FrontendApplication extends TheiaFrontendApplication { export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileService)
protected readonly fileService: FileService;
@inject(WorkspaceService) @inject(WorkspaceService)
private readonly workspaceService: WorkspaceService; protected readonly workspaceService: WorkspaceService;
@inject(CommandService) @inject(CommandService)
private readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(SketchesService) @inject(SketchesService)
private readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
private layoutWasRestored = false;
protected override async initializeLayout(): Promise<void> { protected override async initializeLayout(): Promise<void> {
await super.initializeLayout(); await super.initializeLayout();
@@ -24,16 +26,10 @@ export class FrontendApplication extends TheiaFrontendApplication {
for (const root of roots) { for (const root of roots) {
await this.commandService.executeCommand( await this.commandService.executeCommand(
OpenSketchFiles.Commands.OPEN_SKETCH_FILES.id, OpenSketchFiles.Commands.OPEN_SKETCH_FILES.id,
root.resource, root.resource
!this.layoutWasRestored
); );
this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
} }
}); });
} }
protected override async restoreLayout(): Promise<boolean> {
this.layoutWasRestored = await super.restoreLayout();
return this.layoutWasRestored;
}
} }

View File

@@ -0,0 +1,13 @@
import { injectable } from '@theia/core/shared/inversify';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
@injectable()
export class StatusBarImpl extends TheiaStatusBarImpl {
override async removeElement(id: string): Promise<void> {
await this.ready;
if (this.entries.delete(id)) {
// Unlike Theia, IDE2 updates the status bar only if the element to remove was among the entries. Otherwise, it's a NOOP.
this.update();
}
}
}

View File

@@ -1,35 +1,30 @@
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets'; import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
import { ILogger } from '@theia/core/lib/common/logger';
import { EditorWidget } from '@theia/editor/lib/browser'; import { EditorWidget } from '@theia/editor/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigServiceClient } from '../../config/config-service-client'; import { ConfigService } from '../../../common/protocol/config-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable() @injectable()
export class TabBarDecoratorService extends TheiaTabBarDecoratorService { export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@inject(ConfigServiceClient) @inject(ConfigService)
private readonly configService: ConfigServiceClient; protected readonly configService: ConfigService;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private dataDirUri: URI | undefined; @inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
const fireDidChange = () => this.configService
this.appStateService .getConfiguration()
.reachedState('ready') .then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
.then(() => this.fireDidChangeDecorations()); .catch((err) =>
this.dataDirUri = this.configService.tryGetDataDirUri(); this.logger.error(`Failed to determine the data directory: ${err}`)
this.configService.onDidChangeDataDirUri((dataDirUri) => { );
this.dataDirUri = dataDirUri;
fireDidChange();
});
if (this.dataDirUri) {
fireDidChange();
}
} }
override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] { override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {

View File

@@ -0,0 +1,61 @@
import * as React from '@theia/core/shared/react';
import { injectable } from '@theia/core/shared/inversify';
import { LabelIcon } from '@theia/core/lib/browser/label-parser';
import {
TabBarToolbar as TheiaTabBarToolbar,
TabBarToolbarItem,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
@injectable()
export class TabBarToolbar extends TheiaTabBarToolbar {
/**
* Copied over from Theia. Added an ID to the parent of the toolbar item (`--container`).
* CSS3 does not support parent selectors but we want to style the parent of the toolbar item.
*/
protected override renderItem(item: TabBarToolbarItem): React.ReactNode {
let innerText = '';
const classNames = [];
if (item.text) {
for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${
labelPart.animation ? ' fa-' + labelPart.animation : ''
}`;
classNames.push(...className.split(' '));
} else {
innerText = labelPart;
}
}
}
const command = this.commands.getCommand(item.command);
const iconClass =
(typeof item.icon === 'function' && item.icon()) ||
item.icon ||
(command && command.iconClass);
if (iconClass) {
classNames.push(iconClass);
}
const tooltip = item.tooltip || (command && command.label);
return (
<div
id={`${item.id}--container`}
key={item.id}
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM}${
command && this.commandIsEnabled(command.id) ? ' enabled' : ''
}`}
onMouseDown={this.onMouseDownEvent}
onMouseUp={this.onMouseUpEvent}
onMouseOut={this.onMouseUpEvent}
>
<div
id={item.id}
className={classNames.join(' ')}
onClick={this.executeCommand}
title={tooltip}
>
{innerText}
</div>
</div>
);
}
}

View File

@@ -23,6 +23,11 @@ export class TabBarRenderer extends TheiaTabBarRenderer {
} }
export class ToolbarAwareTabBar extends TheiaToolbarAwareTabBar { export class ToolbarAwareTabBar extends TheiaToolbarAwareTabBar {
protected override async updateBreadcrumbs(): Promise<void> {
// NOOP
// IDE2 does not use breadcrumbs.
}
private readonly doUpdateToolbar = debounce(() => super.updateToolbar(), 500); private readonly doUpdateToolbar = debounce(() => super.updateToolbar(), 500);
protected override updateToolbar(): void { protected override updateToolbar(): void {
// Unlike Theia, IDE2 debounces the toolbar updates with 500ms // Unlike Theia, IDE2 debounces the toolbar updates with 500ms

View File

@@ -1,26 +0,0 @@
import type { Theme } from '@theia/core/lib/common/theme';
import { injectable } from '@theia/core/shared/inversify';
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
export namespace ArduinoThemes {
export const Light: Theme = {
id: 'arduino-theme',
type: 'light',
label: 'Light (Arduino)',
editorTheme: 'arduino-theme',
};
export const Dark: Theme = {
id: 'arduino-theme-dark',
type: 'dark',
label: 'Dark (Arduino)',
editorTheme: 'arduino-theme-dark',
};
}
@injectable()
export class ThemeServiceWithDB extends TheiaThemeServiceWithDB {
protected override init(): void {
this.register(ArduinoThemes.Light, ArduinoThemes.Dark);
super.init();
}
}

View File

@@ -1,3 +1,4 @@
import type { MaybePromise } from '@theia/core';
import type { Widget } from '@theia/core/lib/browser'; import type { Widget } from '@theia/core/lib/browser';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { import {
@@ -7,10 +8,11 @@ import {
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import { EditorWidget } from '@theia/editor/lib/browser'; import { EditorWidget } from '@theia/editor/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget'; import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import deepEqual = require('deep-equal');
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../../sketches-service-client-impl'; } from '../../../common/protocol/sketches-service-client-impl';
@injectable() @injectable()
export class WidgetManager extends TheiaWidgetManager { export class WidgetManager extends TheiaWidgetManager {
@@ -70,4 +72,44 @@ export class WidgetManager extends TheiaWidgetManager {
title.className += title.className + ` ${uncloseableClass}`; title.className += title.className + ` ${uncloseableClass}`;
} }
} }
/**
* Customized to find any existing widget based on `options` deepEquals instead of string equals.
* See https://github.com/eclipse-theia/theia/issues/11309.
*/
protected override doGetWidget<T extends Widget>(
key: string
): MaybePromise<T> | undefined {
const pendingWidget = this.findExistingWidget<T>(key);
if (pendingWidget) {
return pendingWidget as MaybePromise<T>;
}
return undefined;
}
private findExistingWidget<T extends Widget>(
key: string
): MaybePromise<T> | undefined {
const parsed = this.parseJson(key);
for (const [candidateKey, widget] of [
...this.widgetPromises.entries(),
...this.pendingWidgetPromises.entries(),
]) {
const candidate = this.parseJson(candidateKey);
if (deepEqual(candidate, parsed)) {
return widget as MaybePromise<T>;
}
}
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private parseJson(json: string): any {
try {
return JSON.parse(json);
} catch (err) {
console.log(`Failed to parse JSON: <${json}>.`, err);
throw err;
}
}
} }

View File

@@ -1,87 +0,0 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { isOSX } from '@theia/core/lib/common/os';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
@injectable()
export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
@inject(ApplicationServer)
private readonly applicationServer: ApplicationServer;
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;
private _previousRepresentedFilename: string | undefined;
private readonly applicationName =
FrontendApplicationConfigProvider.get().applicationName;
private applicationVersion: string | undefined;
@postConstruct()
protected init(): void {
setTimeout(
() =>
this.applicationServer.getApplicationInfo().then((info) => {
this.applicationVersion = info?.version;
if (this.applicationVersion) {
this.handleWidgetChange(this.applicationShell.currentWidget);
}
}),
0
);
}
protected override handleWidgetChange(widget?: Widget | undefined): void {
if (isOSX) {
this.maybeUpdateRepresentedFilename(widget);
}
// Unlike Theia, IDE2 does not want to show in the window title if the current widget is dirty or not.
// Hence, IDE2 does not track widgets but updates the window title on current widget change.
this.updateTitleWidget(widget);
}
protected override updateTitleWidget(widget?: Widget | undefined): void {
let activeEditorShort = '';
const rootName = this.workspaceService.workspace?.name ?? '';
let appName = `${this.applicationName}${
this.applicationVersion ? ` ${this.applicationVersion}` : ''
}`;
if (rootName) {
appName = ` | ${appName}`;
}
const uri = NavigatableWidget.getUri(widget);
if (uri) {
const base = uri.path.base;
// Do not show the basename of the main sketch file. Only other sketch file names are visible in the title.
if (`${rootName}.ino` !== base) {
activeEditorShort = ` - ${base} `;
}
}
this.windowTitleService.update({ rootName, appName, activeEditorShort });
}
private maybeUpdateRepresentedFilename(widget?: Widget | undefined): void {
if (widget instanceof EditorWidget) {
const { uri } = widget.editor;
const filename = uri.path.toString();
// Do not necessarily require the current window if not needed. It's a synchronous, blocking call.
if (this._previousRepresentedFilename !== filename) {
const currentWindow = remote.getCurrentWindow();
currentWindow.setRepresentedFilename(uri.path.toString());
this._previousRepresentedFilename = filename;
}
}
}
}

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,9 +1,5 @@
import debounce = require('p-debounce'); import debounce = require('p-debounce');
import { import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Event, Emitter } from '@theia/core/lib/common/event'; import { Event, Emitter } from '@theia/core/lib/common/event';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@@ -14,7 +10,7 @@ import { SketchesService } from '../../../common/protocol';
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../../sketches-service-client-impl'; } from '../../../common/protocol/sketches-service-client-impl';
import { DebugConfigurationModel } from './debug-configuration-model'; import { DebugConfigurationModel } from './debug-configuration-model';
import { import {
FileOperationError, FileOperationError,
@@ -130,7 +126,7 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
const uri = tempFolderUri.resolve('launch.json'); const uri = tempFolderUri.resolve('launch.json');
const { value } = await this.fileService.read(uri); const { value } = await this.fileService.read(uri);
const configurations = DebugConfigurationModel.parse(JSON.parse(value)); const configurations = DebugConfigurationModel.parse(JSON.parse(value));
return { uri, configurations, compounds: [] }; return { uri, configurations };
} catch (err) { } catch (err) {
if ( if (
err instanceof FileOperationError && err instanceof FileOperationError &&

View File

@@ -29,7 +29,6 @@ export class DebugConfigurationModel extends TheiaDebugConfigurationModel {
return { return {
uri: this.configUri, uri: this.configUri,
configurations: this.config, configurations: this.config,
compounds: [],
}; };
} }
} }

View File

@@ -1,49 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import {
DebugAdapterPath,
DebugChannel,
ForwardingDebugChannel,
} from '@theia/debug/lib/common/debug-service';
import { DebugSession } from './debug-session';
@injectable()
export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory {
override get(
sessionId: string,
options: DebugConfigurationSessionOptions,
parentSession?: DebugSession
): DebugSession {
const connection = new DebugSessionConnection(
sessionId,
() =>
new Promise<DebugChannel>((resolve) =>
this.connectionProvider.openChannel(
`${DebugAdapterPath}/${sessionId}`,
(wsChannel) => {
resolve(new ForwardingDebugChannel(wsChannel));
},
{ reconnecting: false }
)
),
this.getTraceOutputChannel()
);
// patched debug session
return new DebugSession(
sessionId,
options,
parentSession,
connection,
this.terminalService,
this.editorManager,
this.breakpoints,
this.labelProvider,
this.messages,
this.fileService,
this.debugContributionProvider,
this.workspaceService
);
}
}

View File

@@ -1,120 +1,90 @@
import type { ContextKey } from '@theia/core/lib/browser/context-key-service'; import { injectable } from '@theia/core/shared/inversify';
import { injectable, postConstruct } from '@theia/core/shared/inversify'; import { DebugError } from '@theia/debug/lib/common/debug-service';
import { import { DebugSession } from '@theia/debug/lib/browser/debug-session';
DebugSession, import { DebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
DebugState,
} from '@theia/debug/lib/browser/debug-session';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import type { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; import { nls } from '@theia/core/lib/common';
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() @injectable()
export class DebugSessionManager extends TheiaDebugSessionManager { export class DebugSessionManager extends TheiaDebugSessionManager {
protected debugStateKey: ContextKey<string>; override async start(options: DebugSessionOptions): Promise<DebugSession | undefined> {
return this.progressService.withProgress(
@postConstruct() nls.localize('theia/debug/start', 'Start...'),
protected override init(): void { 'debug',
this.debugStateKey = this.contextKeyService.createKey<string>( async () => {
'debugState', try {
debugStateLabel(this.state) // Only save when dirty. To avoid saving temporary sketches.
); // This is a quick fix for not saving the editor when there are no dirty editors.
super.init(); // // https://github.com/bcmi-labs/arduino-editor/pull/172#issuecomment-741831888
if (this.shell.canSaveAll()) {
await this.shell.saveAll();
} }
await this.fireWillStartDebugSession();
const resolved = await this.resolveConfiguration(options);
protected override fireDidChange(current: DebugSession | undefined): void { //#region "cherry-picked" from here: https://github.com/eclipse-theia/theia/commit/e6b57ba4edabf797f3b4e67bc2968cdb8cc25b1e#diff-08e04edb57cd2af199382337aaf1dbdb31171b37ae4ab38a38d36cd77bc656c7R196-R207
this.debugTypeKey.set(current?.configuration.type); if (!resolved) {
this.inDebugModeKey.set(this.inDebugMode); // As per vscode API: https://code.visualstudio.com/api/references/vscode-api#DebugConfigurationProvider
this.debugStateKey.set(debugStateLabel(this.state)); // "Returning the value 'undefined' prevents the debug session from starting.
this.onDidChangeEmitter.fire(current); // Returning the value 'null' prevents the debug session from starting and opens the
// underlying debug configuration instead."
if (resolved === null) {
this.debugConfigurationManager.openConfiguration();
} }
return undefined;
}
//#endregion end of cherry-pick
protected override async doStart( // preLaunchTask isn't run in case of auto restart as well as postDebugTask
sessionId: string, if (!options.configuration.__restart) {
options: DebugConfigurationSessionOptions const taskRun = await this.runTask(
): Promise<DebugSession> { options.workspaceFolderUri,
const parentSession = resolved.configuration.preLaunchTask,
options.configuration.parentSession && true
this._sessions.get(options.configuration.parentSession.id);
const contrib = this.sessionContributionRegistry.get(
options.configuration.type
); );
const sessionFactory = contrib if (!taskRun) {
? contrib.debugSessionFactory() return undefined;
: 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 const sessionId = await this.debug.createDebugSession(
session.on('exited', async (event) => { resolved.configuration
await session.disconnect(false, () =>
this.debug.terminateDebugSession(session.id)
); );
}); return this.doStart(sessionId, resolved);
} catch (e) {
if (DebugError.NotFound.is(e)) {
this.messageService.error(
nls.localize(
'theia/debug/typeNotSupported',
'The debug session type "{0}" is not supported.',
e.data.type
)
);
return undefined;
}
session.onDispose(() => this.cleanup(session)); this.messageService.error(
session nls.localize(
.start() 'theia/debug/startError',
.then(() => { 'There was an error starting the debug session, check the logs for more details.'
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; console.error('Error starting the debug session', e);
throw e;
}
}
);
}
override async terminateSession(session?: DebugSession): Promise<void> {
if (!session) {
this.updateCurrentSession(this._currentSession);
session = this._currentSession;
}
// The cortex-debug extension does not respond to close requests
// So we simply terminate the debug session immediately
// Alternatively the `super.terminateSession` call will terminate it after 5 seconds without a response
await this.debug.terminateDebugSession(session!.id);
await super.terminateSession(session);
} }
} }

View File

@@ -1,231 +0,0 @@
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Mutable } from '@theia/core/lib/common/types';
import { URI } from '@theia/core/lib/common/uri';
import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint';
import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint';
import {
DebugThreadData,
StoppedDetails,
} from '@theia/debug/lib/browser/model/debug-thread';
import { DebugProtocol } from '@vscode/debugprotocol';
import { DebugThread } from './debug-thread';
export class DebugSession extends TheiaDebugSession {
/**
* The `send('initialize')` request resolves later than `on('initialized')` emits the event.
* Hence, the `configure` would use the empty object `capabilities`.
* Using the empty `capabilities` could result in missing exception breakpoint filters, as
* always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works
* around this timing issue.
* See: https://github.com/eclipse-theia/theia/issues/11886.
*/
protected didReceiveCapabilities = new Deferred();
protected override async initialize(): Promise<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

@@ -0,0 +1,17 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import {
AbstractDialog as TheiaAbstractDialog,
codiconArray,
DialogProps,
} from '@theia/core/lib/browser';
@injectable()
export abstract class AbstractDialog<T> extends TheiaAbstractDialog<T> {
constructor(@inject(DialogProps) protected override readonly props: DialogProps) {
super(props);
this.closeCrossNode.classList.remove(...codiconArray('close'));
this.closeCrossNode.classList.add('fa', 'fa-close');
}
}

View File

@@ -1,63 +0,0 @@
import {
AbstractDialog as TheiaAbstractDialog,
DialogProps,
} from '@theia/core/lib/browser/dialogs';
import { ReactDialog as TheiaReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { codiconArray, Message } from '@theia/core/lib/browser/widgets/widget';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { createRoot } from '@theia/core/shared/react-dom/client';
@injectable()
export abstract class AbstractDialog<T> extends TheiaAbstractDialog<T> {
constructor(
@inject(DialogProps) protected override readonly props: DialogProps
) {
super(props);
this.closeCrossNode.classList.remove(...codiconArray('close'));
this.closeCrossNode.classList.add('fa', 'fa-close');
}
}
@injectable()
export abstract class ReactDialog<T> extends TheiaReactDialog<T> {
protected override onUpdateRequest(msg: Message): void {
// This is tricky to bypass the default Theia code.
// Otherwise, there is a warning when opening the dialog for the second time.
// You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it.
const disposables = new DisposableCollection();
if (!this.isMounted) {
// toggle the `isMounted` logic for the time being of the super call so that the `createRoot` does not run
this.isMounted = true;
disposables.push(Disposable.create(() => (this.isMounted = false)));
}
// Always unset the `contentNodeRoot` so there is no double update when calling super.
const restoreContentNodeRoot = this.contentNodeRoot;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.contentNodeRoot as any) = undefined;
disposables.push(
Disposable.create(() => (this.contentNodeRoot = restoreContentNodeRoot))
);
try {
super.onUpdateRequest(msg);
} finally {
disposables.dispose();
}
// Use the patched rendering.
if (!this.isMounted) {
this.contentNodeRoot = createRoot(this.contentNode);
// Resetting the prop is missing from the Theia code.
// https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/browser/dialogs/react-dialog.tsx#L41-L47
this.isMounted = true;
}
this.contentNodeRoot?.render(<>{this.render()}</>);
}
}

View File

@@ -10,12 +10,4 @@ export class EditorContribution extends TheiaEditorContribution {
): void { ): void {
// NOOP // NOOP
} }
protected override updateEncodingStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
editor: TextEditor | undefined
): void {
// https://github.com/arduino/arduino-ide/issues/1393
// NOOP
}
} }

View File

@@ -6,7 +6,7 @@ import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/l
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../../sketches-service-client-impl'; } from '../../../common/protocol/sketches-service-client-impl';
import { SketchesService, Sketch } from '../../../common/protocol'; import { SketchesService, Sketch } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
@@ -20,7 +20,7 @@ export class EditorWidgetFactory extends TheiaEditorWidgetFactory {
protected override async createEditor( protected override async createEditor(
uri: URI, uri: URI,
options?: NavigatableWidgetOptions options: NavigatableWidgetOptions
): Promise<EditorWidget> { ): Promise<EditorWidget> {
const widget = await super.createEditor(uri, options); const widget = await super.createEditor(uri, options);
return this.maybeUpdateCaption(widget); return this.maybeUpdateCaption(widget);

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