Compare commits

...

34 Commits
2.0.4 ... 2.1.x

Author SHA1 Message Date
Akos Kitta
6499518fa2 chore: Updated to the next version after release.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-30 17:28:10 +02:00
Akos Kitta
c318223de9 chore(cli): Bumped the CLI version to 0.32.3
Ref: arduino/arduino-cli#2234
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-30 16:55:59 +02:00
per1234
2c74ad6437 Don't upload "channel update info files" related to manual builds to S3 on release
Arduino IDE offers an update to the user when a newer version is available. The availability of an update is determined
by comparing the user's IDE version against data file ("channel update info file") stored on Arduino's download server.

These "channel update info files" are automatically generated by the build workflow.

Previously the release process was fully automated, including the upload of the "channel update info files" to the
server.

As a temporary workaround for limitations of the GitHub Actions runner machines used to produce the automated builds,
some release builds are now produced manually:

- Linux build (because the Ubuntu 18.04 runner was shut down and newer runner versions produce builds incompatible with
  older Linux versions)
- macOS Apple Silicon build (because GitHub hosted Apple Silicon runners are not available)

The automatic upload of the "channel update info files" produced by the build workflow is problematic because if users
receive update offers before the "channel update info files" are updated for the manually produced builds, they can
receive an update to a different build than intended:

- Users of older Linux versions would update to a build that won't start on their machine
- macOS Apple Silicon users would update to macOS x86 build that is less performant on their machine

For this reason, the build workflow is adjusted to no longer upload the Linux and macOS "channel update info files" to
the download server on release. These files will now be manually uploaded after they have been updated to provide the
manually produced builds.

This workaround will be reverted once a fully automated release system is regained.
2023-04-18 23:50:14 -07:00
Akos Kitta
9fff553f1a chore: Prepared for the 2.1.0 release
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-17 09:30:59 +02:00
github-actions[bot]
4c2f0fb841 Updated translation files 2023-04-17 08:31:30 +02:00
Akos Kitta
54f210d4de fix: try fetch the sketch by path if not in the cache
The sketch cache might be empty, when trying to generate
the secrets include in the main sketch file from the
`secrets` property.

Closes #1999

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-17 08:23:30 +02:00
Akos Kitta
5540170341 feat: removed the non official themes from the UI
Closes #1283
Ref eclipse-theia/theia#11151

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-14 13:42:40 +02:00
Akos Kitta
7cc252fc36 fix: location of possible drop-in folder for VSIX
Closes #1851

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-14 13:42:40 +02:00
Akos Kitta
cb2a371263 fix: update themeId settings after theme change
In Theia, the theme ID is not always in sync with the persisted
`workbench.colorTheme` preference value. For example, one can preview a
theme with the `CtrlCmd+K` + `CtrlCmd+T` key chords. On quick pick
selection change events, the theme changes, but the change is persisted
only on accept (user presses `Enter`).

IDE2 has its own way of showing and managing different settings in the
UI. When the theme is changed from outside of the IDE2's UI, the model
could get out of sync. This PR ensures that on `workbench.colorTheme`
preference change, IDE2's settings model is synchronized with persisted
Theia preferences.

Closes #1987

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-14 08:56:29 +02:00
Akos Kitta
96da5bb5ea fix: updated to electron-builder@23.6.0 (#1986)
- updated to `electron-builder@23.6.0`
 - set `CSC_FOR_PULL_REQUEST` env to run notarization for a PR build.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-13 17:53:43 +02:00
dependabot[bot]
ef5762599a build(deps): Bump peter-evans/create-pull-request from 4 to 5
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 08:52:14 -07:00
Akos Kitta
3aee575a35 chore(cli): Updated to 0.32.2 CLI
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: per1234 <accounts@perglass.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-13 16:27:47 +02:00
Akos Kitta
80d5b5afa7 fix: propagate monitor errors to the frontend
- Handle when the board's platform is not installed (Closes #1974)
 - UX: Smoother monitor widget reset (Closes #1985)
 - Fixed monitor <input> readOnly state (Closes #1984)
 - Set monitor widget header color (Ref #682)

Closes #1508

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-13 11:58:49 +02:00
Akos Kitta
ab5c63c4b7 Update .github/workflows/build.yml
Co-authored-by: per1234 <accounts@perglass.com>
2023-04-13 11:12:55 +02:00
Akos Kitta
0a53550b0b ci: use ubuntu-latest for the Linux build
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-13 11:12:55 +02:00
Akos Kitta
f5c98c8400 feat(infra): support for topic: cloud label
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-12 19:18:39 +02:00
Akos Kitta
eb1f247296 fix: the focus in the sketchbook widget
Ref: arduino/arduino-ide#1720

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-31 17:12:12 +02:00
Akos Kitta
6e72be1b4c feat: re-focus monitor widget after verify/upload
supported when the monitor widget was the current in the bottom panel,
and the core command (upload/verify/etc./) was successful

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-29 10:02:34 +02:00
Akos Kitta
e4beb03a40 fix: incorrect editor widget key calculation
to avoid duplicate editor tabs when opening a sketch with no previously
saved workbench layout

Closes #1791

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-28 18:56:10 +02:00
Akos Kitta
39ab836880 fix: let the resource finish all write operation
before checking if it's in sync or not.

Closes #437

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-28 18:55:31 +02:00
Akos Kitta
dafb2454fd chore: latest VSIX handles custom directories.data
so when starting the debugger the CLI config path is used by the CLI for
the `daemon -I` command.

Closes #1911

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-28 08:20:11 +02:00
Akos Kitta
9b49712669 feat: omit release details to speed up lib search
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-16 10:01:15 +01:00
Akos Kitta
0ab28266df feat: introduced cloud state in sketchbook view
Closes #1879
Closes #1876
Closes #1899
Closes #1878

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-16 10:00:17 +01:00
dependabot[bot]
b09ae48536 build(deps): Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 16:43:30 -07:00
Akos Kitta
2aad0e3b16 feat: new UX for the boards/library manager widgets
Closes #19
Closes #781
Closes #1591
Closes #1607
Closes #1697
Closes #1707
Closes #1924
Closes #1941

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-15 16:17:05 +01:00
per1234
58aac236bf Allow leading underscore in sketch filenames
The Arduino Sketch Specification defines the allowed format of sketch folder names and sketch code filenames. Arduino
IDE enforces compliance with the specification in order to ensure sketches created with Arduino IDE can be used with any
other Arduino development tool.

The Arduino Sketch Specification has been changed to allow a leading underscore in sketch folder names and sketch code
filenames so IDE's sketch name validation must be updated accordingly.
2023-03-13 10:11:47 -07:00
Akos Kitta
ec24b6813d fix: use text --format for the CLI
`Can't write debug log: available only in text format` error is thrown
by the CLI if the `--debug` flag is present.

Ref: arduino/arduino-cli#2003
Closes #1942

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-13 10:29:17 +01:00
per1234
d398ed1345 Add bundled tools version check step to release procedure
The Arduino IDE release includes several tool dependencies. Unstable versions of these tools may be pinned provisionally
for use with the development version of Arduino IDE, but production releases of Arduino IDE must use production releases
of the tool dependencies.

The release manager should check the tool versions before making a release, but previously this step was not mentioned
in the release procedure documentation.
2023-03-13 00:59:13 -07:00
Akos Kitta
fb10de1446 fix: jsonc parsing in the IDE2 backend
Occurred when `settings.json` contained comments or a trailing comma.

Closes #1945

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-13 08:34:39 +01:00
Akos Kitta
24dc0bbc88 fix: update monitor output after widget show
Closes #1724

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-07 16:11:35 +01:00
Akos Kitta
fa9777e529 fix: scroll to the bottom after the state update
Closes #1736

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-07 16:11:35 +01:00
Akos Kitta
77213507fb fix: encoding when reading a cloud sketch
Closes #449
Closes #634

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-02 09:48:09 +01:00
Akos Kitta
bfec85c352 fix: no unnecessary tree update on mouse over/out
Closes #1766

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-03-02 09:47:48 +01:00
per1234
f3d3d40c75 Bump version metadata post release
On every startup, Arduino IDE checks for new versions of the IDE. If a newer version is available, a notification/dialog
is shown offering an update.

"Newer" is determined by comparing the version of the user's IDE to the latest available version on the update channel.
This comparison is done according to the Semantic Versioning Specification ("SemVer").

In order to facilitate beta testing, builds are generated of the Arduino IDE at the current stage in development. These
builds are given an identifying version of the following form:

- <version>-snapshot-<short hash> - builds generated for every push and pull request that modifies relevant files
- <version>-nightly-<YYYYMMDD> - daily builds of the tip of the default branch

In order to cause these builds to be correctly considered "newer" than the release version, the version metadata must be
bumped immediately following each release.

This will also serve as the metadata bump for the next release in the event that release is a minor release. In case it
is instead a minor or major release, the version metadata will need to be updated once more before the release tag is
created.
2023-02-27 09:08:39 -08:00
167 changed files with 8801 additions and 2400 deletions

View File

@@ -7,6 +7,9 @@
- name: "topic: CLI"
color: "00ffff"
description: Related to Arduino CLI
- name: "topic: cloud"
color: "00ffff"
description: Related to Arduino Cloud and cloud sketches
- name: "topic: debugger"
color: "00ffff"
description: Related to the integrated debugger

View File

@@ -43,7 +43,7 @@ jobs:
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
certificate-extension: pfx # File extension for the certificate.
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
- os: ubuntu-20.04
- os: macos-latest
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
@@ -69,7 +69,7 @@ jobs:
python-version: '3.x'
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
@@ -99,6 +99,7 @@ jobs:
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
export CSC_FOR_PULL_REQUEST=true
fi
if [ "${{ runner.OS }}" = "Windows" ]; then
@@ -244,6 +245,15 @@ jobs:
file_glob: true
body: ${{ needs.changelog.outputs.BODY }}
# Temporary measure to prevent release update offers before the manually produced builds are uploaded.
# The step must be removed once fully automated builds are regained.
- name: Remove "channel update info files" related to manual builds
run: |
# See: https://github.com/arduino/arduino-ide/issues/2018
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-linux.yml"
# See: https://github.com/arduino/arduino-ide/issues/408
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-mac.yml"
- name: Publish Release [S3]
if: github.repository == 'arduino/arduino-ide'
uses: docker://plugins/s3

View File

@@ -36,7 +36,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}

View File

@@ -23,7 +23,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}

View File

@@ -23,7 +23,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
@@ -45,7 +45,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: Updated translation files
title: Update translation files

View File

@@ -25,7 +25,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
@@ -54,7 +54,7 @@ jobs:
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: Updated themes
title: Update themes

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.4",
"version": "2.1.1",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -53,12 +53,10 @@
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
"@types/react-tabs": "^2.3.2",
"@types/react-virtualized": "^9.21.21",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"@vscode/debugprotocol": "^1.51.0",
@@ -67,6 +65,7 @@
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"classnames": "^2.3.1",
"cpy": "^8.1.2",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deepmerge": "2.0.1",
@@ -77,13 +76,14 @@
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-online": "^9.0.1",
"js-yaml": "^3.13.1",
"jsonc-parser": "^2.2.0",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2",
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"open": "^8.0.6",
"p-debounce": "^2.1.0",
@@ -95,7 +95,6 @@
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2",
"react-virtualized": "^9.22.3",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
@@ -121,6 +120,7 @@
"mocha": "^7.0.0",
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"uuid": "^3.2.1",
@@ -163,7 +163,7 @@
],
"arduino": {
"cli": {
"version": "0.31.0"
"version": "0.32.3"
},
"fwuploader": {
"version": "2.2.2"

View File

@@ -79,7 +79,10 @@ import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browse
import { ProblemManager } from './theia/markers/problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import {
ArduinoComponentContextMenuRenderer,
ListItemRenderer,
} from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import {
@@ -90,6 +93,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
import {
FrontendConnectionStatusService,
ApplicationConnectionStatusContribution,
DaemonPort,
IsOnline,
} from './theia/core/connection-status-service';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
@@ -233,7 +238,6 @@ import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate';
import {
ArduinoFirmwareUploader,
@@ -323,9 +327,13 @@ import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ThemeServiceWithDB } from './theia/core/theming';
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
import {
MonacoThemingService,
CleanupObsoleteThemes,
ThemesRegistrationSummary,
MonacoThemeRegistry,
} from './theia/monaco/monaco-theming-service';
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
@@ -350,6 +358,9 @@ import { CreateFeatures } from './create/create-features';
import { Account } from './contributions/account';
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import { CreateCloudCopy } from './contributions/create-cloud-copy';
import { FileResourceResolver } from './theia/filesystem/file-resource';
import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@@ -488,15 +499,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
},
createWidget: () => context.container.get(MonitorWidget),
}));
bind(MonitorManagerProxyFactory).toFactory(
@@ -738,6 +741,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
Contribution.configure(bind, Account);
Contribution.configure(bind, CloudSketchbookContribution);
Contribution.configure(bind, CreateCloudCopy);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -916,8 +921,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(CreateFsProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFsProvider);
bind(FileServiceContribution).toService(CreateFsProvider);
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(CloudSketchbookContribution);
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf();
@@ -973,11 +976,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
bind(ThemeServiceWithDB).toSelf().inSingletonScope();
rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
// workaround for themes cannot be removed after registration
// https://github.com/eclipse-theia/theia/issues/11151
bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(
CleanupObsoleteThemes
);
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
@@ -1021,4 +1032,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SidebarBottomMenuWidget).toSelf();
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
bind(DaemonPort).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(DaemonPort);
bind(IsOnline).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(IsOnline);
// https://github.com/arduino/arduino-ide/issues/437
bind(FileResourceResolver).toSelf().inSingletonScope();
rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
});

View File

@@ -174,7 +174,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
// CLI returns the packages already sorted with the deprecated ones at the end of the list
// in order to ensure the new ones are preferred
const candidates = packagesForBoard.filter(
({ installable, installedVersion }) => installable && !installedVersion
({ installedVersion }) => !installedVersion
);
return candidates[0];

View File

@@ -8,6 +8,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
import { CreateFeatures } from '../create/create-features';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import {
Command,
CommandRegistry,
@@ -29,6 +30,8 @@ export class Account extends Contribution {
private readonly windowService: WindowService;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private readonly toDispose = new DisposableCollection();
private app: FrontendApplication;
@@ -50,21 +53,28 @@ export class Account extends Contribution {
override registerCommands(registry: CommandRegistry): void {
const openExternal = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const loggedIn = () => Boolean(this.createFeatures.session);
const loggedInWithInternetConnection = () =>
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
registry.registerCommand(Account.Commands.LEARN_MORE, {
execute: () => openExternal(LEARN_MORE_URL),
isEnabled: () => !Boolean(this.createFeatures.session),
isEnabled: () => !loggedIn(),
isVisible: () => !loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
execute: () => openExternal('https://id.arduino.cc/'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
execute: () => openExternal('https://create.arduino.cc/editor'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
execute: () => openExternal('https://create.arduino.cc/iot/'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
}

View File

@@ -93,7 +93,7 @@ export abstract class CloudSketchContribution extends SketchContribution {
);
}
try {
await treeModel.sketchbookTree().pull({ node });
await treeModel.sketchbookTree().pull({ node }, true);
return node;
} catch (err) {
if (isNotFound(err)) {

View File

@@ -6,6 +6,10 @@ import {
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MaybePromise } from '@theia/core/lib/common/types';
@@ -14,7 +18,6 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
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 {
MenuModelRegistry,
MenuContribution,
@@ -58,11 +61,12 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
export {
Command,
@@ -218,6 +222,9 @@ export abstract class CoreServiceContribution extends SketchContribution {
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
/**
* This is the internal (Theia) ID of the notification that is currently visible.
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
@@ -280,6 +287,9 @@ export abstract class CoreServiceContribution extends SketchContribution {
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
}): Promise<T> {
const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget()
);
const { progressText, keepOutput, task } = options;
this.outputChannelManager
.getChannel('Arduino')
@@ -291,11 +301,26 @@ export abstract class CoreServiceContribution extends SketchContribution {
run: ({ progressId }) => task(progressId, this.coreService),
keepOutput,
});
toDisposeOnComplete.dispose();
return result;
}
// TODO: cleanup!
// this dependency does not belong here
// support core command contribution handlers, the monitor-widget should implement it and register itself as a handler
// the monitor widget should reveal itself after a successful core command execution
private maybeActivateMonitorWidget(): Disposable {
const currentWidget = this.shell.bottomPanel.currentTitle?.owner;
if (currentWidget?.id === 'serial-monitor') {
return Disposable.create(() =>
this.shell.bottomPanel.activateWidget(currentWidget)
);
}
return Disposable.NULL;
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager.getMessageId({
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Error,

View File

@@ -0,0 +1,118 @@
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell';
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { Progress } 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 { Create } from '../create/typings';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
import {
CreateNewCloudSketchCallback,
NewCloudSketch,
NewCloudSketchParams,
} from './new-cloud-sketch';
import { saveOntoCopiedSketch } from './save-as-sketch';
interface CreateCloudCopyParams {
readonly model: SketchbookTreeModel;
readonly node: SketchbookTree.SketchDirNode;
}
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
return (
typeof arg === 'object' &&
(<CreateCloudCopyParams>arg).model !== undefined &&
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
(<CreateCloudCopyParams>arg).node !== undefined &&
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
);
}
@injectable()
export class CreateCloudCopy extends CloudSketchContribution {
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private shell: ApplicationShell;
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
isEnabled: (args: unknown) =>
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
isVisible: (args: unknown) =>
Boolean(this.createFeatures.enabled) &&
Boolean(this.createFeatures.session) &&
this.connectionStatus.offlineStatus !== 'internet' &&
isCreateCloudCopyParams(args),
});
}
/**
* - creates new cloud sketch with the name of the params sketch,
* - pulls the cloud sketch,
* - copies files from params sketch to pulled cloud sketch in the cache folder,
* - pushes the cloud sketch, and
* - opens in new window.
*/
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
const sketch = await this.sketchesService.loadSketch(
params.node.fileStat.resource.toString()
);
const callback: CreateNewCloudSketchCallback = async (
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
) => {
const treeModel = await this.treeModel();
if (!treeModel) {
throw new Error('Could not retrieve the cloud sketchbook tree model.');
}
progress.report({
message: nls.localize(
'arduino/createCloudCopy/copyingSketchFilesMessage',
'Copying local sketch files...'
),
});
const localCacheFolderUri = newNode.uri.toString();
await this.sketchesService.copy(sketch, {
destinationUri: localCacheFolderUri,
onlySketchFiles: true,
});
await saveOntoCopiedSketch(
sketch,
localCacheFolderUri,
this.shell,
this.editorManager
);
progress.report({ message: pushingSketch(newSketch.name) });
await treeModel.sketchbookTree().push(newNode, true, true);
};
return this.commandService.executeCommand(
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
<NewCloudSketchParams>{
initialValue: params.node.fileStat.name,
callback,
skipShowErrorMessageOnOpen: false,
}
);
}
}
export namespace CreateCloudCopy {
export namespace Commands {
export const CREATE_CLOUD_COPY: Command = {
id: 'arduino-create-cloud-copy',
iconClass: 'fa fa-arduino-cloud-upload',
};
}
}

View File

@@ -1,6 +1,6 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
import {
MenuPath,
CompositeMenuNode,
@@ -11,7 +11,11 @@ import {
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import {
ArduinoMenus,
examplesLabel,
PlaceholderMenuNode,
} from '../menu/arduino-menus';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service';
import {
@@ -25,11 +29,73 @@ import {
SketchRef,
SketchContainer,
SketchesError,
Sketch,
CoreService,
SketchesService,
Sketch,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
import { unregisterSubmenu } from '../menu/arduino-menus';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ApplicationError } from '@theia/core/lib/common/application-error';
/**
* Creates a cloned copy of the example sketch and opens it in a new window.
*/
export async function openClonedExample(
uri: string,
services: {
sketchesService: SketchesService;
commandService: CommandService;
},
onError: {
onDidFailClone?: (
err: ApplicationError<
number,
{
uri: string;
}
>,
uri: string
) => MaybePromise<unknown>;
onDidFailOpen?: (
err: ApplicationError<
number,
{
uri: string;
}
>,
sketch: Sketch
) => MaybePromise<unknown>;
} = {}
): Promise<void> {
const { sketchesService, commandService } = services;
const { onDidFailClone, onDidFailOpen } = onError;
try {
const sketch = await sketchesService.cloneExample(uri);
try {
await commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (openError) {
if (SketchesError.NotFound.is(openError)) {
if (onDidFailOpen) {
await onDidFailOpen(openError, sketch);
return;
}
}
throw openError;
}
} catch (cloneError) {
if (SketchesError.NotFound.is(cloneError)) {
if (onDidFailClone) {
await onDidFailClone(cloneError, uri);
return;
}
}
throw cloneError;
}
}
@injectable()
export abstract class Examples extends SketchContribution {
@@ -94,7 +160,7 @@ export abstract class Examples extends SketchContribution {
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
nls.localize('arduino/examples/menu', 'Examples'),
examplesLabel,
{
order: '4',
}
@@ -174,47 +240,33 @@ export abstract class Examples extends SketchContribution {
}
protected createHandler(uri: string): CommandHandler {
const forceUpdate = () =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
return {
execute: async () => {
const sketch = await this.clone(uri);
if (sketch) {
try {
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
await openClonedExample(
uri,
{
sketchesService: this.sketchesService,
commandService: this.commandRegistry,
},
{
onDidFailClone: () => {
// Do not toast the error message. It's handled by the `Open Sketch` command.
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
forceUpdate();
},
onDidFailOpen: (err) => {
this.messageService.error(err.message);
forceUpdate();
},
}
}
);
},
};
}
private async clone(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.cloneExample(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
}
@injectable()

View File

@@ -6,7 +6,7 @@ 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 { Create, isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
TaskFactoryImpl,
@@ -15,13 +15,36 @@ import {
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { Command, CommandRegistry, Sketch } from './contribution';
import {
CloudSketchContribution,
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch } from './contribution';
export interface CreateNewCloudSketchCallback {
(
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
): Promise<void>;
}
export interface NewCloudSketchParams {
/**
* Value to populate the dialog `<input>` when it opens.
*/
readonly initialValue?: string | undefined;
/**
* Additional callback to call when the new cloud sketch has been created.
*/
readonly callback?: CreateNewCloudSketchCallback;
/**
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
*/
readonly skipShowErrorMessageOnOpen?: boolean;
}
@injectable()
export class NewCloudSketch extends CloudSketchContribution {
@@ -43,7 +66,12 @@ export class NewCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(true),
execute: (params: NewCloudSketchParams) =>
this.createNewSketch(
params?.skipShowErrorMessageOnOpen === false ? false : true,
params?.initialValue,
params?.callback
),
isEnabled: () => Boolean(this.createFeatures.session),
isVisible: () => this.createFeatures.enabled,
});
@@ -66,7 +94,8 @@ export class NewCloudSketch extends CloudSketchContribution {
private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const treeModel = await this.treeModel();
if (treeModel) {
@@ -75,7 +104,8 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode,
treeModel,
skipShowErrorMessageOnOpen,
initialValue
initialValue,
callback
);
}
}
@@ -84,13 +114,14 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.createNewSketchWithProgress(treeModel, value)
this.createNewSketchWithProgress(treeModel, value, callback)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
@@ -118,7 +149,11 @@ export class NewCloudSketch extends CloudSketchContribution {
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(false, taskFactory.value ?? initialValue);
return this.createNewSketch(
false,
taskFactory.value ?? initialValue,
callback
);
}
throw err;
}
@@ -126,7 +161,8 @@ export class NewCloudSketch extends CloudSketchContribution {
private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string
value: string,
callback?: CreateNewCloudSketchCallback
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
@@ -143,6 +179,9 @@ export class NewCloudSketch extends CloudSketchContribution {
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
if (callback && node) {
await callback(sketch, node, progress);
}
return node;
};
}
@@ -152,7 +191,7 @@ export class NewCloudSketch extends CloudSketchContribution {
): Promise<void> {
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
);
}
}

View File

@@ -45,7 +45,11 @@ export class OpenSketchFiles extends SketchContribution {
await this.ensureOpened(uri);
}
if (focusMainSketchFile) {
await this.ensureOpened(mainFileUri, true, { mode: 'activate' });
await this.ensureOpened(mainFileUri, true, {
mode: 'activate',
preview: false,
counter: 0,
});
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(

View File

@@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution {
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node);
await treeModel.sketchbookTree().push(node, true);
// rename
progress.report({

View File

@@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
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 { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTask } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -28,7 +29,7 @@ import {
@injectable()
export class SaveAsSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
@@ -80,14 +81,17 @@ export class SaveAsSketch extends CloudSketchContribution {
return false;
}
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
const copiedSketch = await this.sketchesService.copy(sketch, {
destinationUri,
});
if (!newWorkspaceUri) {
return false;
}
const newWorkspaceUri = copiedSketch.uri;
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
await saveOntoCopiedSketch(
sketch,
newWorkspaceUri,
this.shell,
this.editorManager
);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
@@ -238,53 +242,6 @@ ${dialogContent.question}`.trim();
}
return sketchFolderDestinationUri;
}
private async saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string
): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (
uriString.includes(sketch.uri) &&
saveable &&
saveable.createSnapshot
) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}
}
interface InvalidSketchFolderDialogContent {
@@ -317,3 +274,48 @@ export namespace SaveAsSketch {
};
}
}
export async function saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string,
shell: ApplicationShell,
editorManager: EditorManager
): Promise<void> {
const widgets = shell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}

View File

@@ -2,6 +2,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { fetch } from 'cross-fetch';
import { SketchesService } from '../../common/protocol';
import { uint8ArrayToString } from '../../common/utils';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
@@ -9,59 +10,16 @@ import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { Create, CreateError } from './typings';
export interface ResponseResultProvider {
interface ResponseResultProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(response: Response): Promise<any>;
}
export namespace ResponseResultProvider {
namespace ResponseResultProvider {
export const NOOP: ResponseResultProvider = async () => undefined;
export const TEXT: ResponseResultProvider = (response) => response.text();
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 {
let out, i, c;
let char2, char3;
out = '';
const len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12:
case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
);
break;
}
}
return out;
}
type ResourceType = 'f' | 'd';
@injectable()
@@ -99,6 +57,30 @@ export class CreateApi {
return result;
}
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). If `cache` is `true` and a sketch exists with the path,
* the cache will be updated with the new state of the sketch.
*/
// TODO: no nulls in API
async sketchByPath(
sketchPath: string,
cache = false
): Promise<Create.Sketch | null> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
const sketch = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
if (sketch && cache) {
this.sketchCache.addSketch(sketch);
const posixPath = createPaths.toPosixPath(sketch.path);
this.sketchCache.purgeByPath(posixPath);
}
return sketch;
}
async sketches(limit = 50): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
@@ -128,7 +110,11 @@ export class CreateApi {
async createSketch(
posixPath: string,
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent()
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent(),
payloadOverride: Record<
string,
string | boolean | number | Record<string, unknown>
> = {}
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const [headers, content] = await Promise.all([
@@ -139,6 +125,7 @@ export class CreateApi {
ino: btoa(content),
path: posixPath,
user_id: 'me',
...payloadOverride,
};
const init = {
method: 'PUT',
@@ -254,7 +241,17 @@ export class CreateApi {
return data;
}
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
const posixPath = createPaths.parentPosix(path);
let sketch = this.sketchCache.getSketch(posixPath);
// Workaround for https://github.com/arduino/arduino-ide/issues/1999.
if (!sketch) {
// Convert the ordinary sketch POSIX path to the Create path.
// For example, `/sketch_apr6a` will be transformed to `8a694e4b83878cc53472bd75ee928053:kittaakos/sketches_v2/sketch_apr6a`.
const createPathPrefix = this.sketchCache.createPathPrefix;
if (createPathPrefix) {
sketch = await this.sketchByPath(createPathPrefix + posixPath, true);
}
}
if (
sketch &&
@@ -330,10 +327,9 @@ export class CreateApi {
if (sketch) {
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
const headers = await this.headers();
// parse the secret file
const secrets = (
typeof content === 'string' ? content : Utf8ArrayToStr(content)
typeof content === 'string' ? content : uint8ArrayToString(content)
)
.split(/\r?\n/)
.reduce((prev, curr) => {
@@ -397,7 +393,7 @@ export class CreateApi {
const headers = await this.headers();
let data: string =
typeof content === 'string' ? content : Utf8ArrayToStr(content);
typeof content === 'string' ? content : uint8ArrayToString(content);
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
const payload = { data: btoa(data) };
@@ -491,13 +487,18 @@ export class CreateApi {
await this.run(url, init, ResponseResultProvider.NOOP);
}
private fetchCounter = 0;
private async run<T>(
requestInfo: URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> {
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`);
const fetchCount = `[${++this.fetchCounter}]`;
const fetchStart = performance.now();
const method = init?.method ? `${init.method}: ` : '';
const url = requestInfo.toString();
const response = await fetch(requestInfo.toString(), init);
const fetchEnd = performance.now();
if (!response.ok) {
let details: string | undefined = undefined;
try {
@@ -508,7 +509,18 @@ export class CreateApi {
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
}
const parseStart = performance.now();
const result = await resultProvider(response);
const parseEnd = performance.now();
console.debug(
`HTTP ${fetchCount} ${method} ${url} [fetch: ${(
fetchEnd - fetchStart
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
2
)} ms] body: ${
typeof result === 'string' ? result : JSON.stringify(result)
}`
);
return result;
}

View File

@@ -8,6 +8,9 @@ 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';
import { CreateUri } from './create-uri';
export type CloudSketchState = 'push' | 'pull';
@injectable()
export class CreateFeatures implements FrontendApplicationContribution {
@@ -18,13 +21,22 @@ export class CreateFeatures implements FrontendApplicationContribution {
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
/**
* The keys are the Create URI of the sketches.
*/
private readonly _cloudSketchStates = new Map<string, CloudSketchState>();
private readonly onDidChangeSessionEmitter = new Emitter<
AuthenticationSession | undefined
>();
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
private readonly onDidChangeCloudSketchStateEmitter = new Emitter<{
uri: URI;
state: CloudSketchState | undefined;
}>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeSessionEmitter,
this.onDidChangeEnabledEmitter
this.onDidChangeEnabledEmitter,
this.onDidChangeCloudSketchStateEmitter
);
private _enabled: boolean;
private _session: AuthenticationSession | undefined;
@@ -64,14 +76,46 @@ export class CreateFeatures implements FrontendApplicationContribution {
return this.onDidChangeEnabledEmitter.event;
}
get enabled(): boolean {
return this._enabled;
get onDidChangeCloudSketchState(): Event<{
uri: URI;
state: CloudSketchState | undefined;
}> {
return this.onDidChangeCloudSketchStateEmitter.event;
}
get session(): AuthenticationSession | undefined {
return this._session;
}
get enabled(): boolean {
return this._enabled;
}
cloudSketchState(uri: URI): CloudSketchState | undefined {
return this._cloudSketchStates.get(uri.toString());
}
setCloudSketchState(uri: URI, state: CloudSketchState | undefined): void {
if (uri.scheme !== CreateUri.scheme) {
throw new Error(
`Expected a URI with '${uri.scheme}' scheme. Got: ${uri.toString()}`
);
}
const key = uri.toString();
if (!state) {
if (!this._cloudSketchStates.delete(key)) {
console.warn(
`Could not reset the cloud sketch state of ${key}. No state existed for the the cloud sketch.`
);
} else {
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state: undefined });
}
} else {
this._cloudSketchStates.set(key, state);
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state });
}
}
/**
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
* Returns with `undefined` if `dataDirUri` is `undefined`.
@@ -83,7 +127,10 @@ export class CreateFeatures implements FrontendApplicationContribution {
);
return undefined;
}
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
return dataDirUri
.resolve('RemoteSketchbook')
.resolve('ArduinoCloud')
.isEqualOrParent(new URI(sketch.uri));
}
cloudUri(sketch: Sketch): URI | undefined {

View File

@@ -29,6 +29,7 @@ import { CreateUri } from './create-uri';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { Create } from './typings';
import { stringToUint8Array } from '../../common/utils';
@injectable()
export class CreateFsProvider
@@ -154,7 +155,7 @@ export class CreateFsProvider
async readFile(uri: URI): Promise<Uint8Array> {
const content = await this.getCreateApi.readFile(uri.path.toString());
return new TextEncoder().encode(content);
return stringToUint8Array(content);
}
async writeFile(

View File

@@ -24,6 +24,12 @@ import {
} from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input';
import { InterfaceScale } from '../../contributions/interface-scale';
import {
userConfigurableThemes,
themeLabelForSettings,
arduinoThemeTypeOf,
} from '../../theia/core/theming';
import { Theme } from '@theia/core/lib/common/theme';
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MAX
@@ -218,14 +224,10 @@ export class SettingsComponent extends React.Component<
<div className="flex-line">
<select
className="theia-select"
value={this.props.themeService.getCurrentTheme().label}
value={this.currentThemeLabel}
onChange={this.themeDidChange}
>
{this.props.themeService.getThemes().map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
{this.themeSelectOptions}
</select>
</div>
<div className="flex-line">
@@ -333,6 +335,46 @@ export class SettingsComponent extends React.Component<
);
}
private get currentThemeLabel(): string {
const currentTheme = this.props.themeService.getCurrentTheme();
return themeLabelForSettings(currentTheme);
}
private get separatedThemes(): (Theme | string)[] {
const separatedThemes: (Theme | string)[] = [];
const groupedThemes = userConfigurableThemes(this.props.themeService);
for (const group of groupedThemes) {
for (let i = 0; i < group.length; i++) {
const theme = group[i];
if (i === 0 && separatedThemes.length) {
const arduinoThemeType = arduinoThemeTypeOf(theme);
separatedThemes.push(`separator-${arduinoThemeType}`);
}
separatedThemes.push(theme);
}
}
return separatedThemes;
}
private get themeSelectOptions(): React.ReactNode[] {
return this.separatedThemes.map((item) => {
if (typeof item === 'string') {
return (
// &#9472; -> BOX DRAWINGS LIGHT HORIZONTAL
<option key={item} disabled>
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
</option>
);
}
const label = themeLabelForSettings(item);
return (
<option key={item.id} value={label}>
{label}
</option>
);
});
}
private toSelectOptions(language: string | LanguageInfo): JSX.Element {
const plain = typeof language === 'string';
const key = plain ? language : language.languageId;
@@ -610,8 +652,8 @@ export class SettingsComponent extends React.Component<
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { selectedIndex } = event.target.options;
const theme = this.props.themeService.getThemes()[selectedIndex];
if (theme) {
const theme = this.separatedThemes[selectedIndex];
if (theme && typeof theme !== 'string') {
this.setState({ themeId: theme.id });
if (this.props.themeService.getCurrentTheme().id !== theme.id) {
this.props.themeService.setCurrentTheme(theme.id);

View File

@@ -123,6 +123,17 @@ export class SettingsService {
this._settings = deepClone(settings);
this.ready.resolve();
});
this.preferenceService.onPreferenceChanged(async (event) => {
await this.ready.promise;
const { preferenceName, newValue } = event;
if (
preferenceName === 'workbench.colorTheme' &&
typeof newValue === 'string' &&
this._settings.themeId !== newValue
) {
this.reset();
}
});
}
protected async loadSettings(): Promise<Settings> {

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.355 3.85509L2.85504 14.3551C2.76026 14.448 2.63281 14.5001 2.50006 14.5001C2.36731 14.5001 2.23986 14.448 2.14508 14.3551C2.0514 14.2607 1.99882 14.1331 1.99882 14.0001C1.99882 13.8671 2.0514 13.7395 2.14508 13.6451L3.82508 11.9651C3.24351 11.8742 2.70645 11.5991 2.29291 11.1802C1.87936 10.7613 1.61116 10.2208 1.52775 9.63811C1.44434 9.05543 1.55012 8.46136 1.82955 7.94328C2.10897 7.4252 2.54728 7.01047 3.08 6.76009C3.20492 6.18251 3.47405 5.64596 3.86232 5.20047C4.25058 4.75498 4.74532 4.41505 5.30042 4.21239C5.85552 4.00972 6.45289 3.9509 7.03686 4.04143C7.62082 4.13196 8.17236 4.36887 8.64004 4.73009C9.01346 4.56809 9.41786 4.48995 9.82475 4.50117C10.2316 4.51239 10.6311 4.6127 10.995 4.79503L12.645 3.14509C12.7392 3.05094 12.8669 2.99805 13 2.99805C13.1332 2.99805 13.2609 3.05094 13.355 3.14509C13.4492 3.23924 13.5021 3.36694 13.5021 3.50009C13.5021 3.63324 13.4492 3.76094 13.355 3.85509V3.85509Z" fill="#7F8C8D"/>
<path d="M14.5 9.25047C14.4987 9.97942 14.2086 10.6782 13.6931 11.1936C13.1777 11.709 12.479 11.9992 11.75 12.0005H6.70996L12.355 6.35547C12.38 6.43042 12.4 6.50547 12.4201 6.58044C13.0153 6.72902 13.5436 7.07272 13.9206 7.55669C14.2976 8.04066 14.5016 8.63699 14.5 9.25047V9.25047Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 9.24997C14.4987 9.97893 14.2086 10.6777 13.6932 11.1931C13.1777 11.7086 12.479 11.9987 11.75 12H4.25003C3.62476 11.9998 3.01822 11.7866 2.53034 11.3955C2.04247 11.0045 1.70238 10.4589 1.56612 9.84864C1.42986 9.2384 1.50556 8.59995 1.78074 8.0385C2.05593 7.47705 2.51418 7.0261 3.07998 6.75997C3.2049 6.18239 3.47404 5.64584 3.8623 5.20035C4.25056 4.75486 4.74531 4.41494 5.3004 4.21227C5.8555 4.0096 6.45288 3.95078 7.03684 4.04131C7.62081 4.13184 8.17234 4.36875 8.64003 4.72997C8.99025 4.57772 9.36814 4.49942 9.75003 4.49997C10.3635 4.49838 10.9598 4.70238 11.4438 5.07939C11.9278 5.45641 12.2715 5.9847 12.4201 6.57993C13.0153 6.7285 13.5436 7.07221 13.9206 7.55618C14.2976 8.04015 14.5016 8.63649 14.5 9.24997Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.42 6.58044C12.4 6.50549 12.38 6.43042 12.355 6.35547L11.525 7.18555C11.5575 7.27223 11.6136 7.34811 11.6869 7.40464C11.7603 7.46117 11.8479 7.4961 11.94 7.50549C12.3852 7.55476 12.7947 7.77259 13.0843 8.11428C13.374 8.45597 13.5218 8.89557 13.4975 9.34284C13.4732 9.7901 13.2785 10.2111 12.9536 10.5194C12.6286 10.8276 12.1979 10.9998 11.75 11.0005H7.70996L6.70996 12.0005H11.75C12.421 12.0001 13.0688 11.7545 13.5714 11.3099C14.074 10.8653 14.3969 10.2524 14.4792 9.58644C14.5615 8.92048 14.3977 8.24739 14.0184 7.69379C13.6392 7.14019 13.0708 6.74425 12.42 6.58044V6.58044Z" fill="#7F8C8D"/>
<path d="M13.355 3.14532C13.2606 3.05161 13.133 2.99902 13 2.99902C12.867 2.99902 12.7394 3.05161 12.6451 3.14532L10.995 4.79524C10.6311 4.61291 10.2316 4.5126 9.82472 4.50139C9.41783 4.49017 9.01343 4.56832 8.64002 4.73032C8.17233 4.3691 7.6208 4.13219 7.03684 4.04166C6.45287 3.95114 5.85549 4.00995 5.3004 4.21262C4.7453 4.41529 4.25056 4.75521 3.86229 5.2007C3.47403 5.64619 3.2049 6.18274 3.07997 6.76033C2.54726 7.01071 2.10896 7.42543 1.82954 7.9435C1.55013 8.46157 1.44434 9.05564 1.52775 9.63832C1.61115 10.221 1.87935 10.7615 2.29288 11.1804C2.70641 11.5993 3.24346 11.8744 3.82502 11.9653L2.14502 13.6453C2.05133 13.7397 1.99876 13.8673 1.99876 14.0003C1.99876 14.1333 2.05133 14.2609 2.14502 14.3553C2.23979 14.4482 2.36725 14.5003 2.5 14.5003C2.63275 14.5003 2.7602 14.4482 2.85498 14.3553L13.355 3.85528C13.4487 3.7609 13.5012 3.6333 13.5013 3.50031C13.5013 3.36732 13.4487 3.23972 13.355 3.14532V3.14532ZM4.79006 11.0003H4.25002C3.8356 11.0005 3.43458 10.8535 3.11841 10.5856C2.80224 10.3177 2.59145 9.94623 2.52362 9.5374C2.45578 9.12857 2.53529 8.70893 2.74799 8.35326C2.96069 7.99758 3.29275 7.72898 3.68502 7.59529C3.77434 7.56478 3.85319 7.50962 3.91248 7.43617C3.97176 7.36272 4.00904 7.274 4.02002 7.18025C4.09848 6.57783 4.39334 6.0245 4.84963 5.62341C5.30592 5.22233 5.89251 5.00087 6.50002 5.00032C7.1425 4.99652 7.76054 5.24628 8.21999 5.69539C8.30086 5.77275 8.40511 5.8211 8.5164 5.83285C8.6277 5.8446 8.73974 5.8191 8.83499 5.76033C9.10926 5.58886 9.42655 5.4987 9.75002 5.50032C9.9105 5.50127 10.0702 5.5231 10.225 5.56526L4.79006 11.0003Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.75 12H4.25C3.62484 11.9999 3.01838 11.7868 2.53053 11.3959C2.04269 11.0049 1.70257 10.4595 1.56622 9.84934C1.42987 9.23923 1.50542 8.60087 1.78043 8.03945C2.05543 7.47802 2.51348 7.02702 3.0791 6.76076C3.24864 5.97929 3.68041 5.27932 4.3027 4.77712C4.92499 4.27492 5.70035 4.00071 6.5 4.00002C7.27505 3.99715 8.02853 4.25513 8.63916 4.73244C9.00591 4.57154 9.40329 4.49243 9.8037 4.5006C10.2041 4.50877 10.5979 4.60403 10.9578 4.77976C11.3177 4.9555 11.635 5.20748 11.8876 5.51822C12.1403 5.82895 12.3223 6.19097 12.4209 6.57912C13.0715 6.74324 13.6398 7.13939 14.0188 7.69309C14.3979 8.24679 14.5616 8.91989 14.4792 9.58582C14.3967 10.2518 14.0739 10.8646 13.5713 11.3092C13.0687 11.7538 12.421 11.9995 11.75 12ZM6.5 5.00002C5.89213 5.00017 5.30514 5.22179 4.84885 5.62344C4.39257 6.02508 4.09826 6.57921 4.021 7.18215C4.0093 7.27546 3.97153 7.36357 3.91202 7.43638C3.85252 7.50918 3.77369 7.56374 3.68458 7.59377C3.29236 7.72769 2.9604 7.99647 2.7478 8.35224C2.5352 8.70801 2.45576 9.12768 2.52363 9.53654C2.5915 9.9454 2.80227 10.3169 3.11841 10.5849C3.43455 10.8529 3.83555 11 4.25 11H11.75C12.198 10.9996 12.6289 10.8275 12.9539 10.5191C13.279 10.2108 13.4735 9.7896 13.4975 9.34221C13.5215 8.89481 13.3732 8.45522 13.083 8.11384C12.7929 7.77246 12.3829 7.55524 11.9375 7.50686C11.8238 7.4948 11.7176 7.44411 11.6368 7.36325C11.5559 7.28238 11.5052 7.17624 11.4932 7.06252C11.4474 6.63255 11.2439 6.2348 10.9219 5.94619C10.6 5.65758 10.1824 5.49861 9.75 5.50002C9.42739 5.49791 9.11079 5.58731 8.83692 5.75783C8.74185 5.81746 8.62955 5.84352 8.51794 5.83184C8.40633 5.82015 8.30185 5.77141 8.22119 5.69338C7.76046 5.24569 7.14241 4.99672 6.5 5.00002V5.00002Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -12,11 +12,15 @@ import {
LibrarySearch,
LibraryService,
} from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import {
ListWidget,
UserAbortError,
} from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
import { findChildTheiaButton } from '../utils/dom';
@injectable()
export class LibraryListWidget extends ListWidget<
@@ -141,6 +145,8 @@ export class LibraryListWidget extends ListWidget<
// All
installDependencies = true;
}
} else {
throw new UserAbortError();
}
} else {
// The lib does not have any dependencies.
@@ -235,6 +241,11 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
this.response = 0;
super.handleEnter(event);
}
protected override onAfterAttach(message: Message): void {
super.onAfterAttach(message);
findChildTheiaButton(this.controlPanel)?.focus();
}
}
export namespace MessageBoxDialog {
export interface Options extends DialogProps {

View File

@@ -1,4 +1,3 @@
import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import {
MAIN_MENU_BAR,
@@ -7,6 +6,8 @@ import {
MenuPath,
SubMenuOptions,
} from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls';
import { isOSX } from '@theia/core/lib/common/os';
export namespace ArduinoMenus {
// Main menu
@@ -173,6 +174,17 @@ export namespace ArduinoMenus {
'3_sign_out',
];
// Context menu from the library and boards manager widget
export const ARDUINO_COMPONENT__CONTEXT = ['arduino-component--context'];
export const ARDUINO_COMPONENT__CONTEXT__INFO_GROUP = [
...ARDUINO_COMPONENT__CONTEXT,
'0_info',
];
export const ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP = [
...ARDUINO_COMPONENT__CONTEXT,
'1_action',
];
// -- ROOT SSL CERTIFICATES
export const ROOT_CERTIFICATES__CONTEXT = [
'arduino-root-certificates--context',
@@ -230,3 +242,5 @@ export class PlaceholderMenuNode implements MenuNode {
return [...this.menuPath, 'placeholder'].join('-');
}
}
export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples');

View File

@@ -1,11 +1,14 @@
import {
CommandRegistry,
ApplicationError,
Disposable,
Emitter,
MessageService,
nls,
} from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { Board, Port } from '../common/protocol';
import {
Monitor,
@@ -23,21 +26,31 @@ import { BoardsServiceProvider } from './boards/boards-service-provider';
export class MonitorManagerProxyClientImpl
implements MonitorManagerProxyClient
{
@inject(MessageService)
private readonly messageService: MessageService;
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
private readonly server: MonitorManagerProxyFactory;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
// When pluggable monitor messages are received from the backend
// this event is triggered.
// Ideally a frontend component is connected to this event
// to update the UI.
protected readonly onMessagesReceivedEmitter = new Emitter<{
private readonly onMessagesReceivedEmitter = new Emitter<{
messages: string[];
}>();
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
protected readonly onMonitorSettingsDidChangeEmitter =
private readonly onMonitorSettingsDidChangeEmitter =
new Emitter<MonitorSettings>();
readonly onMonitorSettingsDidChange =
this.onMonitorSettingsDidChangeEmitter.event;
protected readonly onMonitorShouldResetEmitter = new Emitter();
private readonly onMonitorShouldResetEmitter = new Emitter<void>();
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;
// WebSocket used to handle pluggable monitor communication between
@@ -51,29 +64,16 @@ export class MonitorManagerProxyClientImpl
return this.wsPort;
}
constructor(
@inject(MessageService)
protected messageService: MessageService,
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
protected server: MonitorManagerProxyFactory,
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {}
/**
* Connects a localhost WebSocket using the specified port.
* @param addressPort port of the WebSocket
*/
async connect(addressPort: number): Promise<void> {
if (!!this.webSocket) {
if (this.wsPort === addressPort) return;
else this.disconnect();
if (this.webSocket) {
if (this.wsPort === addressPort) {
return;
}
this.disconnect();
}
try {
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
@@ -87,6 +87,9 @@ export class MonitorManagerProxyClientImpl
return;
}
const opened = new Deferred<void>();
this.webSocket.onopen = () => opened.resolve();
this.webSocket.onerror = () => opened.reject();
this.webSocket.onmessage = (message) => {
const parsedMessage = JSON.parse(message.data);
if (Array.isArray(parsedMessage))
@@ -99,19 +102,26 @@ export class MonitorManagerProxyClientImpl
}
};
this.wsPort = addressPort;
return opened.promise;
}
/**
* Disconnects the WebSocket if connected.
*/
disconnect(): void {
if (!this.webSocket) return;
if (!this.webSocket) {
return;
}
this.onBoardsConfigChanged?.dispose();
this.onBoardsConfigChanged = undefined;
try {
this.webSocket?.close();
this.webSocket.close();
this.webSocket = undefined;
} catch {
} catch (err) {
console.error(
'Could not close the websocket connection for the monitor.',
err
);
this.messageService.error(
nls.localize(
'arduino/monitor/unableToCloseWebSocket',
@@ -126,6 +136,7 @@ export class MonitorManagerProxyClientImpl
}
async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
await this.boardsServiceProvider.reconciled;
this.lastConnectedBoard = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
@@ -150,11 +161,11 @@ export class MonitorManagerProxyClientImpl
? Port.keyOf(this.lastConnectedBoard.selectedPort)
: undefined)
) {
this.onMonitorShouldResetEmitter.fire(null);
this.lastConnectedBoard = {
selectedBoard: selectedBoard,
selectedPort: selectedPort,
};
this.onMonitorShouldResetEmitter.fire();
} else {
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
// recreate the listener callback
@@ -167,7 +178,14 @@ export class MonitorManagerProxyClientImpl
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
await this.server().startMonitor(selectedBoard, selectedPort, settings);
try {
this.clearVisibleNotification();
await this.server().startMonitor(selectedBoard, selectedPort, settings);
} catch (err) {
const message = ApplicationError.is(err) ? err.message : String(err);
this.previousNotificationId = this.notificationId(message);
this.messageService.error(message);
}
}
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
@@ -199,4 +217,24 @@ export class MonitorManagerProxyClientImpl
})
);
}
/**
* This is the internal (Theia) ID of the notification that is currently visible.
* It's stored here as a field to be able to close it before starting a new monitor connection. It's a hack.
*/
private previousNotificationId: string | undefined;
private clearVisibleNotification(): void {
if (this.previousNotificationId) {
this.notificationManager.clear(this.previousNotificationId);
this.previousNotificationId = undefined;
}
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Error,
});
}
}

View File

@@ -4,7 +4,14 @@ import {
LocalStorageService,
} from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonitorManagerProxyClient } from '../common/protocol';
import {
isMonitorConnected,
MonitorConnectionStatus,
monitorConnectionStatusEquals,
MonitorEOL,
MonitorManagerProxyClient,
MonitorState,
} from '../common/protocol';
import { isNullOrUndefined } from '../common/utils';
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
@@ -19,36 +26,36 @@ export class MonitorModel implements FrontendApplicationContribution {
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
MonitorState.Change<keyof MonitorState>
>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _lineEnding: MonitorModel.EOL;
protected _lineEnding: MonitorEOL;
protected _interpolate: boolean;
protected _darkTheme: boolean;
protected _wsPort: number;
protected _serialPort: string;
protected _connected: boolean;
protected _connectionStatus: MonitorConnectionStatus;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._interpolate = false;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this._lineEnding = MonitorEOL.DEFAULT;
this._darkTheme = false;
this._wsPort = 0;
this._serialPort = '';
this._connected = true;
this._connectionStatus = 'not-connected';
this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
MonitorState.Change<keyof MonitorState>
>();
}
onStart(): void {
this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.getData<MonitorState>(MonitorModel.STORAGE_ID)
.then(this.restoreState.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
@@ -56,11 +63,11 @@ export class MonitorModel implements FrontendApplicationContribution {
);
}
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
get onChange(): Event<MonitorState.Change<keyof MonitorState>> {
return this.onChangeEmitter.event;
}
protected restoreState(state: MonitorModel.State): void {
protected restoreState(state: MonitorState): void {
if (!state) {
return;
}
@@ -125,11 +132,11 @@ export class MonitorModel implements FrontendApplicationContribution {
this.timestamp = !this._timestamp;
}
get lineEnding(): MonitorModel.EOL {
get lineEnding(): MonitorEOL {
return this._lineEnding;
}
set lineEnding(lineEnding: MonitorModel.EOL) {
set lineEnding(lineEnding: MonitorEOL) {
if (lineEnding === this._lineEnding) return;
this._lineEnding = lineEnding;
this.monitorManagerProxy.changeSettings({
@@ -211,19 +218,26 @@ export class MonitorModel implements FrontendApplicationContribution {
);
}
get connected(): boolean {
return this._connected;
get connectionStatus(): MonitorConnectionStatus {
return this._connectionStatus;
}
set connected(connected: boolean) {
if (connected === this._connected) return;
this._connected = connected;
set connectionStatus(connectionStatus: MonitorConnectionStatus) {
if (
monitorConnectionStatusEquals(connectionStatus, this.connectionStatus)
) {
return;
}
this._connectionStatus = connectionStatus;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { connected },
monitorUISettings: {
connectionStatus,
connected: isMonitorConnected(connectionStatus),
},
});
this.onChangeEmitter.fire({
property: 'connected',
value: this._connected,
property: 'connectionStatus',
value: this._connectionStatus,
});
}
@@ -238,7 +252,7 @@ export class MonitorModel implements FrontendApplicationContribution {
darkTheme,
wsPort,
serialPort,
connected,
connectionStatus,
} = monitorUISettings;
if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
@@ -248,31 +262,7 @@ export class MonitorModel implements FrontendApplicationContribution {
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
if (!isNullOrUndefined(connected)) this.connected = connected;
if (!isNullOrUndefined(connectionStatus))
this.connectionStatus = connectionStatus;
};
}
// TODO: Move this to /common
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
lineEnding: EOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connected: boolean;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}
export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}

View File

@@ -10,6 +10,7 @@ import {
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { Event } from '@theia/core/lib/common/event';
import { MonitorModel } from '../../monitor-model';
import { MonitorManagerProxyClient } from '../../../common/protocol';
@@ -84,13 +85,13 @@ export class MonitorViewContribution
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
onDidChange: this.model.onChange as Event<unknown> as Event<void>,
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
onDidChange: this.model.onChange as Event<unknown> as Event<void>,
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
@@ -143,8 +144,7 @@ export class MonitorViewContribution
protected async reset(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
await this.openView({ activate: true, reveal: true });
widget.reset();
}
}

View File

@@ -1,7 +1,14 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import {
ReactWidget,
Message,
@@ -13,9 +20,13 @@ import { SerialMonitorSendInput } from './serial-monitor-send-input';
import { SerialMonitorOutput } from './serial-monitor-send-output';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { nls } from '@theia/core/lib/common';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import {
MonitorEOL,
MonitorManagerProxyClient,
} from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class MonitorWidget extends ReactWidget {
@@ -40,40 +51,46 @@ export class MonitorWidget extends ReactWidget {
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor(
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel,
@inject(MonitorModel)
private readonly monitorModel: MonitorModel;
@inject(MonitorManagerProxyClient)
private readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient,
private readonly toDisposeOnReset: DisposableCollection;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {
constructor() {
super();
this.id = MonitorWidget.ID;
this.title.label = MonitorWidget.LABEL;
this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDisposeOnReset = new DisposableCollection();
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => this.monitorManagerProxy.disconnect())
);
}
protected override onBeforeAttach(msg: Message): void {
this.update();
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
this.getCurrentSettings().then(this.onMonitorSettingsDidChange.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
this.monitorManagerProxy.startMonitor();
@postConstruct()
protected init(): void {
this.toDisposeOnReset.dispose();
this.toDisposeOnReset.pushAll([
Disposable.create(() => this.monitorManagerProxy.disconnect()),
this.monitorModel.onChange(() => this.update()),
this.monitorManagerProxy.onMonitorSettingsDidChange((event) =>
this.updateSettings(event)
),
]);
this.startMonitor();
}
onMonitorSettingsDidChange(settings: MonitorSettings): void {
reset(): void {
this.init();
}
private updateSettings(settings: MonitorSettings): void {
this.settings = {
...this.settings,
pluggableMonitorSettings: {
@@ -90,6 +107,7 @@ export class MonitorWidget extends ReactWidget {
}
override dispose(): void {
this.toDisposeOnReset.dispose();
super.dispose();
}
@@ -117,7 +135,12 @@ export class MonitorWidget extends ReactWidget {
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.update();
}
protected onFocusResolved = (element: HTMLElement | undefined): void => {
if (this.closing || !this.isAttached) {
return;
}
@@ -127,7 +150,7 @@ export class MonitorWidget extends ReactWidget {
);
};
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorModel.EOL>[] {
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorEOL>[] {
return [
{
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),
@@ -151,11 +174,23 @@ export class MonitorWidget extends ReactWidget {
];
}
private getCurrentSettings(): Promise<MonitorSettings> {
private async startMonitor(): Promise<void> {
await this.appStateService.reachedState('ready');
await this.boardsServiceProvider.reconciled;
await this.syncSettings();
await this.monitorManagerProxy.startMonitor();
}
private async syncSettings(): Promise<void> {
const settings = await this.getCurrentSettings();
this.updateSettings(settings);
}
private async getCurrentSettings(): Promise<MonitorSettings> {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
if (!board || !port) {
return Promise.resolve(this.settings || {});
return this.settings || {};
}
return this.monitorManagerProxy.getCurrentSettings(board, port);
}
@@ -166,7 +201,7 @@ export class MonitorWidget extends ReactWidget {
: undefined;
const baudrateOptions = baudrate?.values.map((b) => ({
label: b + ' baud',
label: nls.localize('arduino/monitor/baudRate', '{0} baud', b),
value: b,
}));
const baudrateSelectedOption = baudrateOptions?.find(
@@ -176,7 +211,7 @@ export class MonitorWidget extends ReactWidget {
const lineEnding =
this.lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || this.lineEndings[1]; // Defaults to `\n`.
) || MonitorEOL.DEFAULT;
return (
<div className="serial-monitor">
@@ -223,13 +258,13 @@ export class MonitorWidget extends ReactWidget {
);
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
protected readonly onSend = (value: string): void => this.doSend(value);
protected doSend(value: string): void {
this.monitorManagerProxy.send(value);
}
protected readonly onChangeLineEnding = (
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
option: SerialMonitorOutput.SelectOption<MonitorEOL>
): void => {
this.monitorModel.lineEnding = option.value;
};

View File

@@ -5,6 +5,10 @@ import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
import {
isMonitorConnectionError,
MonitorConnectionStatus,
} from '../../../common/protocol';
class HistoryList {
private readonly items: string[] = [];
@@ -62,7 +66,7 @@ export namespace SerialMonitorSendInput {
}
export interface State {
text: string;
connected: boolean;
connectionStatus: MonitorConnectionStatus;
history: HistoryList;
}
}
@@ -75,18 +79,27 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: true, history: new HistoryList() };
this.state = {
text: '',
connectionStatus: 'not-connected',
history: new HistoryList(),
};
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
override componentDidMount(): void {
this.setState({ connected: this.props.monitorModel.connected });
this.setState({
connectionStatus: this.props.monitorModel.connectionStatus,
});
this.toDisposeBeforeUnmount.push(
this.props.monitorModel.onChange(({ property }) => {
if (property === 'connected')
this.setState({ connected: this.props.monitorModel.connected });
if (property === 'connected' || property === 'connectionStatus') {
this.setState({
connectionStatus: this.props.monitorModel.connectionStatus,
});
}
})
);
}
@@ -97,44 +110,83 @@ export class SerialMonitorSendInput extends React.Component<
}
override render(): React.ReactNode {
const status = this.state.connectionStatus;
const input = this.renderInput(status);
if (status !== 'connecting') {
return input;
}
return <label>{input}</label>;
}
private renderInput(status: MonitorConnectionStatus): React.ReactNode {
const inputClassName = this.inputClassName(status);
const placeholder = this.placeholder;
const readOnly = Boolean(inputClassName);
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
placeholder={this.placeholder}
value={this.state.text}
className={`theia-input ${inputClassName}`}
readOnly={readOnly}
placeholder={placeholder}
title={placeholder}
value={readOnly ? '' : this.state.text} // always show the placeholder if cannot edit the <input>
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
);
}
private inputClassName(
status: MonitorConnectionStatus
): 'error' | 'warning' | '' {
if (isMonitorConnectionError(status)) {
return 'error';
}
if (status === 'connected') {
return '';
}
return 'warning';
}
protected shouldShowWarning(): boolean {
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return !this.state.connected || !board || !port;
return !this.state.connectionStatus || !board || !port;
}
protected get placeholder(): string {
if (this.shouldShowWarning()) {
const status = this.state.connectionStatus;
if (isMonitorConnectionError(status)) {
return status.errorMessage;
}
if (status === 'not-connected') {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
);
}
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
const boardLabel = board
? Board.toString(board, {
useFqbn: false,
})
: Unknown;
const portLabel = port ? port.address : Unknown;
if (status === 'connecting') {
return nls.localize(
'arduino/serial/connecting',
"Connecting to '{0}' on '{1}'...",
boardLabel,
portLabel
);
}
return nls.localize(
'arduino/serial/message',
"Message (Enter to send message to '{0}' on '{1}')",
board
? Board.toString(board, {
useFqbn: false,
})
: Unknown,
port ? port.address : Unknown
boardLabel,
portLabel
);
}

View File

@@ -17,7 +17,7 @@ export class SerialMonitorOutput extends React.Component<
* Do not touch it. It is used to be able to "follow" the serial monitor log.
*/
protected toDisposeBeforeUnmount = new DisposableCollection();
private listRef: React.RefObject<any>;
private listRef: React.RefObject<List>;
constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props);
@@ -34,12 +34,10 @@ export class SerialMonitorOutput extends React.Component<
<List
className="serial-monitor-messages"
height={this.props.height}
itemData={
{
lines: this.state.lines,
timestamp: this.state.timestamp,
} as any
}
itemData={{
lines: this.state.lines,
timestamp: this.state.timestamp,
}}
itemCount={this.state.lines.length}
itemSize={18}
width={'100%'}
@@ -65,11 +63,13 @@ export class SerialMonitorOutput extends React.Component<
this.state.charCount
);
const [lines, charCount] = truncateLines(newLines, totalCharCount);
this.setState({
lines,
charCount,
});
this.scrollToBottom();
this.setState(
{
lines,
charCount,
},
() => this.scrollToBottom()
);
}),
this.props.clearConsoleEvent(() =>
this.setState({ lines: [], charCount: 0 })
@@ -91,11 +91,11 @@ export class SerialMonitorOutput extends React.Component<
this.toDisposeBeforeUnmount.dispose();
}
scrollToBottom = ((): void => {
private readonly scrollToBottom = () => {
if (this.listRef.current && this.props.monitorModel.autoscroll) {
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
}
}).bind(this);
};
}
const _Row = ({

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="#626262"><path d="M16 7.992C16 3.58 12.416 0 8 0S0 3.58 0 7.992c0 2.43 1.104 4.62 2.832 6.09c.016.016.032.016.032.032c.144.112.288.224.448.336c.08.048.144.111.224.175A7.98 7.98 0 0 0 8.016 16a7.98 7.98 0 0 0 4.48-1.375c.08-.048.144-.111.224-.16c.144-.111.304-.223.448-.335c.016-.016.032-.016.032-.032c1.696-1.487 2.8-3.676 2.8-6.106zm-8 7.001c-1.504 0-2.88-.48-4.016-1.279c.016-.128.048-.255.08-.383a4.17 4.17 0 0 1 .416-.991c.176-.304.384-.576.64-.816c.24-.24.528-.463.816-.639c.304-.176.624-.304.976-.4A4.15 4.15 0 0 1 8 10.342a4.185 4.185 0 0 1 2.928 1.166c.368.368.656.8.864 1.295c.112.288.192.592.24.911A7.03 7.03 0 0 1 8 14.993zm-2.448-7.4a2.49 2.49 0 0 1-.208-1.024c0-.351.064-.703.208-1.023c.144-.32.336-.607.576-.847c.24-.24.528-.431.848-.575c.32-.144.672-.208 1.024-.208c.368 0 .704.064 1.024.208c.32.144.608.336.848.575c.24.24.432.528.576.847c.144.32.208.672.208 1.023c0 .368-.064.704-.208 1.023a2.84 2.84 0 0 1-.576.848a2.84 2.84 0 0 1-.848.575a2.715 2.715 0 0 1-2.064 0a2.84 2.84 0 0 1-.848-.575a2.526 2.526 0 0 1-.56-.848zm7.424 5.306c0-.032-.016-.048-.016-.08a5.22 5.22 0 0 0-.688-1.406a4.883 4.883 0 0 0-1.088-1.135a5.207 5.207 0 0 0-1.04-.608a2.82 2.82 0 0 0 .464-.383a4.2 4.2 0 0 0 .624-.784a3.624 3.624 0 0 0 .528-1.934a3.71 3.71 0 0 0-.288-1.47a3.799 3.799 0 0 0-.816-1.199a3.845 3.845 0 0 0-1.2-.8a3.72 3.72 0 0 0-1.472-.287a3.72 3.72 0 0 0-1.472.288a3.631 3.631 0 0 0-1.2.815a3.84 3.84 0 0 0-.8 1.199a3.71 3.71 0 0 0-.288 1.47c0 .352.048.688.144 1.007c.096.336.224.64.4.927c.16.288.384.544.624.784c.144.144.304.271.48.383a5.12 5.12 0 0 0-1.04.624c-.416.32-.784.703-1.088 1.119a4.999 4.999 0 0 0-.688 1.406c-.016.032-.016.064-.016.08C1.776 11.636.992 9.91.992 7.992C.992 4.14 4.144.991 8 .991s7.008 3.149 7.008 7.001a6.96 6.96 0 0 1-2.032 4.907z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -165,7 +165,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
border: 1px solid var(--theia-arduino-toolbar-dropdown-border);
display: flex;
gap: 10px;
height: 28px;
height: var(--arduino-button-height);
margin: 0 4px;
overflow: hidden;
padding: 0 10px;

View File

@@ -15,10 +15,10 @@
.p-TabBar-tabIcon.cloud-sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./cloud-sketchbook-tree-icon.svg);
-webkit-mask: url(../icons/arduino-cloud.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
width: var(--theia-icon-size);
width: 19px !important;
height: var(--theia-icon-size);
-webkit-mask-size: 100%;
}
@@ -26,7 +26,7 @@
.p-mod-current
.cloud-sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./cloud-sketchbook-tree-icon-filled.svg);
-webkit-mask: url(../icons/arduino-cloud-filled.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100%;
@@ -99,26 +99,7 @@
color: var(--theia-textLink-foreground);
}
.pull-sketch-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./pull-sketch-icon.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.push-sketch-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./push-sketch-icon.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.account-icon {
background: url("./account-icon.svg") center center no-repeat;
width: var(--theia-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size);
border-radius: 50%;
@@ -199,3 +180,12 @@
.arduino-share-sketch-dialog .sketch-link-embed textarea {
width: 100%;
}
.actions.item.flex-line .fa,
.theia-file-icons-js.file-icon .fa {
font-size: var(--theia-icon-size);
}
.theia-file-icons-js.file-icon.not-in-sync-offline .fa {
color: var(--theia-activityBar-inactiveForeground);
}

View File

@@ -12,7 +12,7 @@
min-width: 424px;
max-height: 560px;
padding: 0 28px;
padding: 0 var(--arduino-button-height);
}
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
@@ -35,7 +35,7 @@
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent > input {
margin-bottom: 28px;
margin-bottom: var(--arduino-button-height);
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
@@ -43,7 +43,7 @@
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
margin-top: 28px;
margin-top: var(--arduino-button-height);
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
margin-top: 0;

View File

@@ -1,14 +1,19 @@
@font-face {
font-family: 'Open Sans';
src: url('fonts/OpenSans-Regular-webfont.woff') format('woff');
}
@font-face {
font-family: 'Open Sans Bold';
src: url('fonts/OpenSans-Bold-webfont.woff') format('woff');
}
@font-face {
font-family: 'FontAwesome';
src:
url('fonts/FontAwesome.ttf?2jhpmq') format('truetype'),
url('fonts/FontAwesome.woff?2jhpmq') format('woff'),
url('fonts/FontAwesome.svg?2jhpmq#FontAwesome') format('svg');
url('fonts/FontAwesome.ttf?h959em') format('truetype'),
url('fonts/FontAwesome.woff?h959em') format('woff'),
url('fonts/FontAwesome.svg?h959em#FontAwesome') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -674,3 +679,21 @@
.fa-microchip:before {
content: "\f2db";
}
.fa-arduino-cloud-download:before {
content: "\e910";
}
.fa-arduino-cloud-upload:before {
content: "\e914";
}
.fa-arduino-cloud:before {
content: "\e915";
}
.fa-arduino-cloud-filled:before {
content: "\e912";
}
.fa-arduino-cloud-offline:before {
content: "\e913";
}
.fa-arduino-cloud-filled-offline:before {
content: "\e911";
}

View File

@@ -23,6 +23,12 @@
<glyph unicode="&#xe90d;" glyph-name="arduino-monitor" horiz-adv-x="1536" d="M651.891 59.977c-92.835 0-179.095 28.493-250.5 77.197l-129.659-129.658c-22.494-22.496-58.964-22.496-81.458 0s-22.494 58.963 0 81.459l124.954 124.954c-67.75 78.157-108.777 180.090-108.777 291.489 0 245.759 199.68 445.439 445.44 445.439s445.44-199.679 445.44-445.439c0-245.761-199.68-445.441-445.44-445.441zM651.891 797.257c-161.28 0-291.84-130.559-291.84-291.839s130.56-291.841 291.84-291.841c160.512 0 291.84 130.561 291.84 291.841 0 160.511-130.56 291.839-291.84 291.839zM1149.562 472.766c0-35.423 28.717-64.138 64.141-64.138s64.134 28.716 64.134 64.138c0 35.423-28.71 64.139-64.134 64.139s-64.141-28.716-64.141-64.139zM64.064 408.62c-35.382 0-64.064 28.682-64.064 64.063s28.682 64.064 64.064 64.064c35.381 0 64.064-28.682 64.064-64.064s-28.683-64.063-64.064-64.063zM1458.707 408.628c-35.418 0-64.134 28.716-64.134 64.138s28.717 64.139 64.134 64.139c35.424 0 64.141-28.716 64.141-64.139s-28.717-64.138-64.141-64.138zM652.659 424.010c-44.961 0-81.408 36.447-81.408 81.407s36.447 81.408 81.408 81.408c44.96 0 81.408-36.447 81.408-81.408s-36.448-81.407-81.408-81.407z" />
<glyph unicode="&#xe90e;" glyph-name="arduino-sketch-tabs-menu" d="M511.998 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432zM923.433 347.425c50.494 0 91.432 40.936 91.432 91.432s-40.937 91.432-91.432 91.432c-50.494 0-91.432-40.936-91.432-91.432s40.937-91.432 91.432-91.432zM100.565 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432z" />
<glyph unicode="&#xe90f;" glyph-name="arduino-plotter" horiz-adv-x="862" d="M323.368-19.351c-20.263 0-39 11.42-48.21 29.788l-146.789 293.581h-74.474c-29.789 0-53.895 24.107-53.895 53.895s24.105 53.895 53.895 53.895h107.789c20.421 0 39.053-11.528 48.21-29.788l96.527-193.056 180.263 720.949c5.842 23.579 26.737 40.263 51 40.842 23.947 1.579 45.893-15.158 52.894-38.421l150.162-500.526h67.681c29.788 0 53.895-24.107 53.895-53.895s-24.107-53.895-53.895-53.895h-107.789c-23.789 0-44.787 15.629-51.631 38.422l-105.316 351.104-168.052-672.053c-5.474-21.897-23.948-38.055-46.368-40.529-2-0.21-3.947-0.313-5.895-0.313h-0.001z" />
<glyph unicode="&#xe910;" glyph-name="arduino-cloud-download" d="M684.256 156.891l-146.286-146.286c-6.932-6.802-16.255-10.606-25.964-10.606s-19.032 3.803-25.964 10.606l-146.286 146.286c-3.41 3.41-6.115 7.458-7.96 11.913s-2.796 9.23-2.796 14.052c-0.001 9.738 3.868 19.079 10.754 25.965s16.226 10.756 25.964 10.756c4.822 0 9.597-0.949 14.052-2.795s8.504-4.549 11.914-7.959l83.749-84.107v423.856c0 9.699 3.853 19.002 10.712 25.86s16.16 10.712 25.86 10.712c9.699 0 19.001-3.853 25.86-10.712s10.712-16.16 10.712-25.86v-423.856l83.749 84.107c6.886 6.886 16.227 10.756 25.966 10.756s19.079-3.869 25.966-10.756c6.886-6.886 10.755-16.227 10.755-25.966s-3.869-19.079-10.755-25.966zM786.286 292.572h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.953-4.363-15.168-6.269-23.332-5.414s-15.805 4.42-21.704 10.128c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
<glyph unicode="&#xe911;" glyph-name="arduino-cloud-filled-offline" d="M854.72 704.131l-671.997-672.001c-6.066-5.946-14.223-9.28-22.719-9.28s-16.653 3.334-22.719 9.28c-5.996 6.042-9.361 14.208-9.361 22.72s3.365 16.678 9.361 22.72l107.52 107.52c-37.22 5.818-71.592 23.424-98.059 50.234s-43.632 61.402-48.97 98.694c-5.338 37.292 1.432 75.312 19.315 108.469s45.935 59.7 80.029 75.724c7.995 36.965 25.219 71.304 50.068 99.816s56.512 50.267 92.038 63.237c35.526 12.971 73.758 16.735 111.132 10.941s72.672-20.956 102.604-44.074c23.899 10.368 49.78 15.369 75.821 14.651 26.038-0.718 51.606-7.138 74.896-18.807l105.6 105.596c6.029 6.026 14.202 9.411 22.72 9.411 8.525 0 16.698-3.385 22.72-9.411 6.029-6.026 9.414-14.198 9.414-22.72s-3.386-16.694-9.414-22.72v0zM928 358.827c-0.083-46.653-18.65-91.375-51.642-124.36-32.986-32.986-77.702-51.558-124.358-51.642h-322.563l361.283 361.282c1.6-4.797 2.88-9.6 4.166-14.398 38.093-9.509 71.904-31.506 96.032-62.48s37.184-69.139 37.082-108.402v0z" />
<glyph unicode="&#xe912;" glyph-name="arduino-cloud-filled" d="M928 358.859c-0.083-46.653-18.65-91.375-51.635-124.36-32.992-32.992-77.709-51.558-124.365-51.642h-479.998c-40.017 0.013-78.836 13.658-110.060 38.688-31.224 25.024-52.989 59.942-61.71 98.999-8.721 39.055-3.876 79.916 13.736 115.849s46.94 64.794 83.151 81.826c7.995 36.965 25.22 71.304 50.068 99.816s56.513 50.266 92.038 63.237c35.526 12.971 73.759 16.735 111.132 10.941s72.672-20.956 102.604-44.074c22.414 9.744 46.599 14.755 71.040 14.72 39.262 0.102 77.425-12.954 108.401-37.083s52.973-57.94 62.483-96.035c38.093-9.508 71.904-31.506 96.032-62.48s37.184-69.14 37.082-108.403z" />
<glyph unicode="&#xe913;" glyph-name="arduino-cloud-offline" d="M794.88 529.709c-1.28 4.797-2.56 9.601-4.16 14.398l-53.12-53.125c2.080-5.548 5.67-10.404 10.362-14.022 4.698-3.618 10.304-5.853 16.198-6.454 28.493-3.153 54.701-17.094 73.235-38.963 18.541-21.868 28-50.003 26.445-78.628s-14.016-55.569-34.81-75.3c-20.8-19.725-48.365-30.746-77.030-30.79h-258.563l-64-64h322.563c42.944 0.026 84.403 15.744 116.57 44.198s52.832 67.68 58.099 110.301c5.267 42.621-5.216 85.699-29.491 121.13-24.269 35.43-60.646 60.771-102.298 71.254v0zM854.72 749.557c-6.042 5.997-14.208 9.363-22.72 9.363s-16.678-3.366-22.714-9.363l-105.606-105.595c-23.29 11.669-48.858 18.089-74.898 18.806s-51.923-4.284-75.821-14.652c-29.932 23.118-65.23 38.28-102.604 44.074s-75.606 2.029-111.132-10.941c-35.526-12.971-67.19-34.726-92.039-63.237s-42.073-62.851-50.068-99.816c-34.093-16.024-62.145-42.566-80.028-75.723s-24.653-71.177-19.315-108.468c5.338-37.292 22.502-71.884 48.968-98.693s60.837-44.416 98.057-50.234l-107.52-107.52c-5.996-6.042-9.361-14.208-9.361-22.72s3.364-16.678 9.361-22.72c6.065-5.946 14.223-9.28 22.719-9.28s16.653 3.334 22.719 9.28l672.001 672.001c5.997 6.040 9.357 14.207 9.363 22.718 0 8.511-3.366 16.678-9.363 22.719v0zM306.564 246.838h-34.563c-26.523-0.013-52.188 9.395-72.423 26.541s-33.725 40.92-38.067 67.085c-4.342 26.165 0.747 53.022 14.36 75.785s34.865 39.954 59.97 48.51c5.716 1.953 10.763 5.483 14.557 10.184s6.18 10.379 6.883 16.379c5.021 38.555 23.892 73.968 53.095 99.638s66.744 39.843 105.625 39.878c41.119 0.243 80.673-15.741 110.078-44.484 5.176-4.951 11.848-8.045 18.97-8.797s14.294 0.88 20.39 4.641c17.553 10.974 37.86 16.744 58.562 16.641 10.271-0.061 20.492-1.458 30.399-4.156l-347.836-347.843z" />
<glyph unicode="&#xe914;" glyph-name="arduino-cloud-upload" d="M684.258 412.892c-6.932-6.799-16.255-10.607-25.964-10.607s-19.032 3.809-25.964 10.607l-83.751 84.118v-423.867c0-9.699-3.853-19.003-10.711-25.856-6.859-6.861-16.161-10.715-25.86-10.715s-19.001 3.855-25.86 10.715c-6.859 6.853-10.712 16.157-10.712 25.856v423.867l-83.749-84.118c-6.886-6.886-16.227-10.756-25.966-10.756s-19.079 3.869-25.966 10.756c-6.886 6.886-10.755 16.227-10.755 25.966s3.869 19.079 10.755 25.966l146.286 146.286c6.903 6.854 16.236 10.701 25.964 10.701s19.062-3.847 25.964-10.701l146.286-146.286c6.853-6.904 10.7-16.237 10.701-25.965s-3.845-19.062-10.698-25.966zM786.286 256.001h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.954-4.362-15.168-6.268-23.331-5.413s-15.805 4.419-21.705 10.127c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
<glyph unicode="&#xe915;" glyph-name="arduino-cloud" d="M752 182.857h-480c-40.010 0.006-78.824 13.645-110.046 38.662-31.222 25.024-52.989 59.93-61.716 98.98-8.726 39.047-3.891 79.902 13.709 115.833s46.915 64.796 83.115 81.836c10.851 50.014 38.484 94.812 78.31 126.953s89.45 49.69 140.627 49.734c49.603 0.184 97.826-16.327 136.906-46.875 23.472 10.298 48.904 15.361 74.531 14.838s50.829-6.62 73.862-17.866c23.034-11.247 43.341-27.374 59.507-47.261 16.173-19.887 27.821-43.056 34.131-67.898 41.638-10.504 78.010-35.857 102.266-71.294 24.262-35.437 34.739-78.515 29.466-121.135-5.28-42.623-25.939-81.842-58.106-110.296s-73.619-44.179-116.563-44.211zM416 630.856c-38.904-0.010-76.471-14.193-105.674-39.899s-48.038-61.169-52.982-99.757c-0.749-5.972-3.166-11.611-6.975-16.271s-8.853-8.151-14.556-10.073c-25.102-8.571-46.348-25.773-59.954-48.542s-18.691-49.628-14.347-75.795c4.344-26.167 17.833-49.943 38.066-67.095s45.897-26.566 72.422-26.566h480c28.672 0.026 56.25 11.040 77.050 30.778 20.806 19.731 33.254 46.688 34.79 75.321s-7.955 56.767-26.528 78.616c-18.566 21.848-44.806 35.75-73.312 38.847-7.277 0.772-14.074 4.016-19.245 9.191-5.178 5.176-8.422 11.969-9.19 19.247-2.931 27.518-15.955 52.974-36.563 71.445-20.602 18.471-47.328 28.645-75.002 28.555-20.647 0.135-40.909-5.587-58.437-16.5-6.084-3.816-13.272-5.484-20.415-4.737s-13.83 3.868-18.992 8.861c-29.487 28.652-69.042 44.586-110.156 44.375v0z" />
<glyph unicode="&#xf001;" glyph-name="music" horiz-adv-x="878" d="M877.714 822.857v-640c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v306.857l-438.857-135.429v-405.143c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v552.571c0 24 16 45.143 38.857 52.571l475.429 146.286c5.143 1.714 10.286 2.286 16 2.286 30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf002;" glyph-name="search" horiz-adv-x="951" d="M658.286 475.428c0 141.143-114.857 256-256 256s-256-114.857-256-256 114.857-256 256-256 256 114.857 256 256zM950.857 0c0-40-33.143-73.143-73.143-73.143-19.429 0-38.286 8-51.429 21.714l-196 195.429c-66.857-46.286-146.857-70.857-228-70.857-222.286 0-402.286 180-402.286 402.286s180 402.286 402.286 402.286 402.286-180 402.286-402.286c0-81.143-24.571-161.143-70.857-228l196-196c13.143-13.143 21.143-32 21.143-51.429z" />
<glyph unicode="&#xf003;" glyph-name="envelope-o" d="M950.857 91.428v438.857c-12-13.714-25.143-26.286-39.429-37.714-81.714-62.857-164-126.857-243.429-193.143-42.857-36-96-80-155.429-80h-1.143c-59.429 0-112.571 44-155.429 80-79.429 66.286-161.714 130.286-243.429 193.143-14.286 11.429-27.429 24-39.429 37.714v-438.857c0-9.714 8.571-18.286 18.286-18.286h841.143c9.714 0 18.286 8.571 18.286 18.286zM950.857 692c0 14.286 3.429 39.429-18.286 39.429h-841.143c-9.714 0-18.286-8.571-18.286-18.286 0-65.143 32.571-121.714 84-162.286 76.571-60 153.143-120.571 229.143-181.143 30.286-24.571 85.143-77.143 125.143-77.143h1.143c40 0 94.857 52.571 125.143 77.143 76 60.571 152.571 121.143 229.143 181.143 37.143 29.143 84 92.571 84 141.143zM1024 713.143v-621.714c0-50.286-41.143-91.429-91.429-91.429h-841.143c-50.286 0-91.429 41.143-91.429 91.429v621.714c0 50.286 41.143 91.429 91.429 91.429h841.143c50.286 0 91.429-41.143 91.429-91.429z" />

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -15,7 +15,7 @@
}
.ide-updater-dialog--logo-container {
margin-right: 28px;
margin-right: var(--arduino-button-height);
}
.ide-updater-dialog--logo {
@@ -76,7 +76,7 @@
.ide-updater-dialog .buttons-container {
display: flex;
justify-content: flex-end;
margin-top: 28px;
margin-top: var(--arduino-button-height);
}
.ide-updater-dialog .buttons-container a.theia-button {

View File

@@ -20,14 +20,20 @@
@import './progress-bar.css';
@import './settings-step-input.css';
:root {
--arduino-button-height: 28px;
}
/* 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);
--theia-icon-loading-warning: url(../icons/loading-dark.svg);
}
body.theia-dark {
--theia-icon-loading: url(../icons/loading-dark.svg);
--theia-icon-loading-warning: url(../icons/loading-light.svg);
}
.theia-input.warning:focus {
@@ -44,29 +50,39 @@ body.theia-dark {
}
.theia-input.warning::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
opacity: 1; /* Firefox */
}
.theia-input.warning:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
.theia-input.warning::-ms-input-placeholder {
/* Microsoft Edge */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
.hc-black.hc-theia.theia-hc .theia-input.warning,
.hc-black.hc-theia.theia-hc .theia-input.warning::placeholder {
color: var(--theia-warningBackground);
background-color: var(--theia-warningForeground);
}
.theia-input.error:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
color: var(--theia-errorForeground);
background-color: var(--theia-errorBackground);
}
.theia-input.error {
background-color: var(--theia-errorBackground);
}
.theia-input.error::placeholder {
color: var(--theia-errorForeground);
background-color: var(--theia-errorBackground);
}
/* Makes the sidepanel a bit wider when opening the widget */
.p-DockPanel-widget {
min-width: 200px;
min-width: 220px;
min-height: 20px;
height: 200px;
height: 220px;
}
/* Overrule the default Theia CSS button styles. */
@@ -74,9 +90,9 @@ button.theia-button,
.theia-button {
align-items: center;
display: flex;
font-family: 'Open Sans',sans-serif;
font-family: 'Open Sans Bold',sans-serif;
font-style: normal;
font-weight: 700;
font-weight: 700;
font-size: 14px;
justify-content: center;
cursor: pointer;
@@ -95,7 +111,7 @@ button.theia-button,
}
button.theia-button {
height: 28px;
height: var(--arduino-button-height);
max-width: none;
}
@@ -154,10 +170,6 @@ button.theia-button.message-box-dialog-button {
font-size: 14px;
}
.uppercase {
text-transform: uppercase;
}
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc button.theia-button:hover,

View File

@@ -44,102 +44,152 @@
height: 100%; /* This has top be 100% down to the `scrollContainer`. */
}
.filterable-list-container .items-container > div > div:nth-child(odd) {
background-color: var(--theia-sideBar-background);
filter: contrast(105%);
}
.filterable-list-container .items-container > div > div:nth-child(even) {
background-color: var(--theia-sideBar-background);
filter: contrast(95%);
}
.filterable-list-container .items-container > div > div:hover {
background-color: var(--theia-sideBar-background);
filter: contrast(90%);
}
.component-list-item {
padding: 10px 10px 10px 15px;
font-size: var(--theia-ui-font-size1);
}
.component-list-item:hover {
cursor: pointer;
padding: 20px 15px 25px;
}
.component-list-item .header {
padding-bottom: 2px;
display: flex;
flex-direction: column;
min-height: var(--theia-statusBar-height);
}
.component-list-item .header .version-info {
.component-list-item .header > div {
display: flex;
}
.component-list-item .header > div .p-TabBar-toolbar {
align-self: start;
padding: unset;
margin-right: unset;
}
.component-list-item:hover .header > div .p-TabBar-toolbar > div {
visibility: visible;
}
.component-list-item .header > div .p-TabBar-toolbar > div {
visibility: hidden;
}
.component-list-item .header .title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
}
.component-list-item .header .title .name {
font-family: 'Open Sans Bold';
font-style: normal;
font-weight: 700;
font-size: 14px;
}
.component-list-item .header .version {
display: flex;
justify-content: space-between;
align-items: center;
}
.component-list-item .header .name {
font-weight: bold;
}
.component-list-item .header .author {
font-weight: bold;
color: var(--theia-panelTitle-inactiveForeground);
}
.component-list-item:hover .header .author {
color: var(--theia-foreground);
}
.component-list-item .header .version {
color: var(--theia-panelTitle-inactiveForeground);
padding-top: 4px;
}
.component-list-item .footer .theia-button.install {
height: auto; /* resets the default Theia button height in the filterable list widget */
}
.component-list-item .header .installed:before {
margin-left: 4px;
.component-list-item .header .installed-version:before {
min-width: 79px;
display: inline-block;
justify-self: end;
background-color: var(--theia-button-background);
text-align: center;
background-color: var(--theia-arduino-toolbar-dropdown-option-backgroundHover);
padding: 2px 4px 2px 4px;
font-size: 10px;
font-weight: bold;
font-size: 12px;
max-height: calc(1em + 4px);
color: var(--theia-button-foreground);
content: attr(install);
}
.component-list-item .header .installed:hover:before {
background-color: var(--theia-button-foreground);
color: var(--theia-button-background);
content: attr(uninstall);
content: attr(version);
cursor: pointer;
border-radius: 4px;
}
.component-list-item[min-width~="170px"] .footer {
padding: 5px 5px 0px 0px;
min-height: 35px;
.component-list-item .header .installed-version:hover:before {
content: attr(remove);
text-transform: uppercase;
}
.component-list-item .content {
display: flex;
flex-direction: row-reverse;
flex-direction: column;
padding-top: 4px;
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-size: 12px;
}
.component-list-item .content > p {
margin-block-start: unset;
margin-block-end: unset;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.component-list-item .content > .info {
white-space: nowrap;
}
.component-list-item .footer {
flex-direction: column-reverse;
padding-top: 8px;
}
.component-list-item .footer > * {
display: inline-block;
margin: 5px 0px 0px 10px;
}
.filterable-list-container .separator {
display: flex;
flex-direction: row;
}
.filterable-list-container .separator :last-child,
.filterable-list-container .separator :first-child {
min-height: 8px;
max-height: 8px;
min-width: 8px;
max-width: 8px;
}
div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :first-child,
div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :last-child {
display: none;
}
.filterable-list-container .separator .line {
max-height: 1px;
height: 1px;
background-color: var(--theia-activityBar-inactiveForeground);
flex: 1 1 auto;
}
.component-list-item:hover .footer > label {
display: inline-block;
align-self: center;
margin: 5px 0px 0px 10px;
}
.component-list-item .info a {
@@ -151,13 +201,33 @@
text-decoration: underline;
}
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:hover:before {
background-color: transparent;
outline: 1px dashed var(--theia-focusBorder);
.component-list-item .theia-button.secondary.no-border {
border: 2px solid var(--theia-button-foreground)
}
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
.component-list-item .theia-button.secondary.no-border:hover {
border: 2px solid var(--theia-secondaryButton-foreground)
}
.component-list-item .theia-button {
margin-left: 12px;
}
.component-list-item .theia-select {
height: var(--arduino-button-height);
min-height: var(--arduino-button-height);
width: 65px;
min-width: 65px;
}
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:hover:before {
background-color: transparent;
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:before {
color: var(--theia-button-background);
border: 1px solid var(--theia-button-border);
}

View File

@@ -28,8 +28,8 @@
display: flex;
justify-content: center;
align-items: center;
height: 28px;
width: 28px;
height: var(--arduino-button-height);
width: var(--arduino-button-height);
}
.p-TabBar-toolbar .item.arduino-tool-item .arduino-upload-sketch--toolbar,
@@ -66,8 +66,8 @@
}
.arduino-tool-icon {
height: 28px;
width: 28px;
height: var(--arduino-button-height);
width: var(--arduino-button-height);
}
.arduino-verify-sketch--toolbar-icon {
@@ -77,7 +77,7 @@
.arduino-upload-sketch--toolbar-icon {
-webkit-mask: url(../icons/upload.svg) center no-repeat;
background-color: var(--theia-titleBar-activeBackground);
background-color: var(--theia-titleBar-activeBackground);
}
.toggle-serial-monitor-icon {
@@ -114,6 +114,10 @@
z-index: 0;
}
.p-TabBar-toolbar .item > div {
text-align: center;
}
:root {
--theia-private-menubar-height: 40px; /* set the topbar height */
}

View File

@@ -20,22 +20,47 @@
.serial-monitor .head {
display: flex;
padding: 5px;
padding: 0px 5px 5px 0px;
height: 27px;
background-color: var(--theia-activityBar-background);
}
.serial-monitor .head .send {
display: flex;
flex: 1;
margin-right: 2px;
}
.serial-monitor .head .send > input {
.serial-monitor .head .send > label:before {
content: "";
position: absolute;
top: -1px;
background: var(--theia-icon-loading-warning) center center no-repeat;
animation: theia-spin 1.25s linear infinite;
width: 30px;
height: 30px;
}
.serial-monitor .head .send > label {
position: relative;
width: 100%;
display: flex;
align-self: baseline;
}
.serial-monitor .head .send > input,
.serial-monitor .head .send > label > input {
line-height: var(--theia-content-line-height);
height: 27px;
width: 100%;
}
.serial-monitor .head .send > input:focus {
.serial-monitor .head .send > label > input {
padding-left: 30px;
box-sizing: border-box;
}
.serial-monitor .head .send > input:focus,
.serial-monitor .head .send > label > input:focus {
border-color: var(--theia-focusBorder);
}

View File

@@ -1,4 +0,0 @@
<svg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 6.18999C2.415 6.18999 2.33 6.16999 2.25 6.12499C1.17 5.49999 0.5 4.33999 0.5 3.09499C0.5 1.84999 1.17 0.689992 2.25 0.0649925C2.49 -0.0700075 2.795 0.00999246 2.935 0.249992C3.07 0.489992 2.99 0.794992 2.75 0.934992C1.98 1.37499 1.5 2.20499 1.5 3.09499C1.5 3.98499 1.98 4.81499 2.75 5.25499C2.99 5.39499 3.07 5.69999 2.935 5.93999C2.84 6.09999 2.675 6.18999 2.5 6.18999Z" fill="#008184"/>
<path d="M5.49993 6.18999C5.32493 6.18999 5.15993 6.09999 5.06493 5.93999C4.92493 5.69999 5.00993 5.39499 5.24993 5.25499C6.01993 4.81499 6.49993 3.98499 6.49993 3.09499C6.49993 2.20499 6.01993 1.37499 5.24993 0.934992C5.00993 0.794992 4.92993 0.489992 5.06493 0.249992C5.20493 0.00999246 5.50993 -0.0700075 5.74993 0.0649925C6.82993 0.689992 7.49993 1.84999 7.49993 3.09499C7.49993 4.33999 6.82993 5.49999 5.74993 6.12499C5.66993 6.16999 5.58493 6.18999 5.49993 6.18999Z" fill="#008184"/>
</svg>

Before

Width:  |  Height:  |  Size: 992 B

View File

@@ -3,13 +3,6 @@
mask: url('./sketchbook.svg');
}
.sketch-folder-icon {
background: url('./sketch-folder-icon.svg') center center no-repeat;
background-position-x: 1px;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.p-TabBar-tabIcon.sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./sketchbook-tree-icon.svg);

View File

@@ -1,106 +1,324 @@
import {
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { Disposable } 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 { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { Disposable } from '@theia/core/lib/common/disposable';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { ArduinoDaemon } from '../../../common/protocol';
import { assertUnreachable } from '../../../common/utils';
import { CreateFeatures } from '../../create/create-features';
import { NotificationCenter } from '../../notification-center';
import { nls } from '@theia/core/lib/common';
import debounce = require('lodash.debounce');
import isOnline = require('is-online');
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
export class IsOnline implements FrontendApplicationContribution {
private readonly onDidChangeOnlineEmitter = new Emitter<boolean>();
private _online = false;
private stopped = false;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
onStart(): void {
const checkOnline = async () => {
if (!this.stopped) {
try {
const online = await isOnline();
this.setOnline(online);
} finally {
window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval
}
}
};
checkOnline();
}
protected connectedPort: string | undefined;
onStop(): void {
this.stopped = true;
this.onDidChangeOnlineEmitter.dispose();
}
@postConstruct()
protected override async init(): Promise<void> {
this.schedulePing();
try {
this.connectedPort = await this.daemon.tryGetPort();
} catch {}
this.notificationCenter.onDaemonDidStart(
(port) => (this.connectedPort = port)
);
this.notificationCenter.onDaemonDidStop(
() => (this.connectedPort = undefined)
);
const refresh = debounce(() => {
this.updateStatus(!!this.connectedPort);
this.schedulePing();
}, this.options.offlineTimeout - 10);
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
get online(): boolean {
return this._online;
}
get onDidChangeOnline(): Event<boolean> {
return this.onDidChangeOnlineEmitter.event;
}
private setOnline(online: boolean) {
const oldOnline = this._online;
this._online = online;
if (!this.stopped && this._online !== oldOnline) {
this.onDidChangeOnlineEmitter.fire(this._online);
}
}
}
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
export class DaemonPort implements FrontendApplicationContribution {
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
private readonly daemon: ArduinoDaemon;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
protected connectedPort: string | undefined;
private readonly onPortDidChangeEmitter = new Emitter<string | undefined>();
private _port: string | undefined;
onStart(): void {
this.daemon.tryGetPort().then(
(port) => this.setPort(port),
(reason) =>
console.warn('Could not retrieve the CLI daemon port.', reason)
);
this.notificationCenter.onDaemonDidStart((port) => this.setPort(port));
this.notificationCenter.onDaemonDidStop(() => this.setPort(undefined));
}
onStop(): void {
this.onPortDidChangeEmitter.dispose();
}
get port(): string | undefined {
return this._port;
}
get onDidChangePort(): Event<string | undefined> {
return this.onPortDidChangeEmitter.event;
}
private setPort(port: string | undefined): void {
const oldPort = this._port;
this._port = port;
if (this._port !== oldPort) {
this.onPortDidChangeEmitter.fire(this._port);
}
}
}
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@postConstruct()
protected async init(): Promise<void> {
protected override async init(): Promise<void> {
this.schedulePing();
const refresh = debounce(() => {
this.updateStatus(Boolean(this.daemonPort.port) && this.isOnline.online);
this.schedulePing();
}, this.options.offlineTimeout - 10);
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
}
protected override async performPingRequest(): Promise<void> {
try {
this.connectedPort = await this.daemon.tryGetPort();
} catch {}
this.notificationCenter.onDaemonDidStart(
(port) => (this.connectedPort = port)
);
this.notificationCenter.onDaemonDidStop(
() => (this.connectedPort = undefined)
);
await this.pingService.ping();
this.updateStatus(this.isOnline.online);
} catch (e) {
this.updateStatus(false);
this.logger.error(e);
}
}
}
const connectionStatusStatusBar = 'connection-status';
const theiaOffline = 'theia-mod-offline';
export type OfflineConnectionStatus =
/**
* There is no websocket connection between the frontend and the backend.
*/
| 'backend'
/**
* The CLI daemon port is not available. Could not establish the gRPC connection between the backend and the CLI.
*/
| 'daemon'
/**
* Cloud not connect to the Internet from the browser.
*/
| 'internet';
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
private readonly offlineStatusDidChangeEmitter = new Emitter<
OfflineConnectionStatus | undefined
>();
private noInternetConnectionNotificationId: string | undefined;
private _offlineStatus: OfflineConnectionStatus | undefined;
get offlineStatus(): OfflineConnectionStatus | undefined {
return this._offlineStatus;
}
get onOfflineStatusDidChange(): Event<OfflineConnectionStatus | undefined> {
return this.offlineStatusDidChangeEmitter.event;
}
protected override onStateChange(state: ConnectionStatus): void {
if (!this.connectedPort && state === ConnectionStatus.ONLINE) {
if (
(!Boolean(this.daemonPort.port) || !this.isOnline.online) &&
state === ConnectionStatus.ONLINE
) {
return;
}
super.onStateChange(state);
}
protected override handleOffline(): void {
this.statusBar.setElement('connection-status', {
const params = {
port: this.daemonPort.port,
online: this.isOnline.online,
};
this._offlineStatus = offlineConnectionStatusType(params);
const { text, tooltip } = offlineMessage(params);
this.statusBar.setElement(connectionStatusStatusBar, {
alignment: StatusBarAlignment.LEFT,
text: this.connectedPort
? nls.localize('theia/core/offline', 'Offline')
: '$(bolt) ' +
nls.localize('theia/core/daemonOffline', 'CLI Daemon Offline'),
tooltip: this.connectedPort
? nls.localize(
'theia/core/cannotConnectBackend',
'Cannot connect to the backend.'
)
: nls.localize(
'theia/core/cannotConnectDaemon',
'Cannot connect to the CLI daemon.'
),
text,
tooltip,
priority: 5000,
});
this.toDisposeOnOnline.push(
Disposable.create(() => this.statusBar.removeElement('connection-status'))
);
document.body.classList.add('theia-mod-offline');
this.toDisposeOnOnline.push(
document.body.classList.add(theiaOffline);
this.toDisposeOnOnline.pushAll([
Disposable.create(() =>
document.body.classList.remove('theia-mod-offline')
)
);
this.statusBar.removeElement(connectionStatusStatusBar)
),
Disposable.create(() => document.body.classList.remove(theiaOffline)),
Disposable.create(() => {
this._offlineStatus = undefined;
this.fireStatusDidChange();
}),
]);
if (!this.isOnline.online) {
const text = nls.localize(
'arduino/connectionStatus/connectionLost',
"Connection lost. Cloud sketch actions and updates won't be available."
);
this.noInternetConnectionNotificationId = this.notificationManager[
'getMessageId'
]({ text, type: MessageType.Warning });
if (this.createFeatures.enabled) {
this.messageService.warn(text);
}
this.toDisposeOnOnline.push(
Disposable.create(() => this.clearNoInternetConnectionNotification())
);
}
this.fireStatusDidChange();
}
private clearNoInternetConnectionNotification(): void {
if (this.noInternetConnectionNotificationId) {
this.notificationManager.clear(this.noInternetConnectionNotificationId);
this.noInternetConnectionNotificationId = undefined;
}
}
private fireStatusDidChange(): void {
if (this.createFeatures.enabled) {
return this.offlineStatusDidChangeEmitter.fire(this._offlineStatus);
}
}
}
interface OfflineMessageParams {
readonly port: string | undefined;
readonly online: boolean;
}
interface OfflineMessage {
readonly text: string;
readonly tooltip: string;
}
/**
* (non-API) exported for testing
*
* The precedence of the offline states are the following:
* - No connection to the Theia backend,
* - CLI daemon is offline, and
* - There is no Internet connection.
*/
export function offlineMessage(params: OfflineMessageParams): OfflineMessage {
const statusType = offlineConnectionStatusType(params);
const text = getOfflineText(statusType);
const tooltip = getOfflineTooltip(statusType);
return { text, tooltip };
}
function offlineConnectionStatusType(
params: OfflineMessageParams
): OfflineConnectionStatus {
const { port, online } = params;
if (port && online) {
return 'backend';
}
if (!port) {
return 'daemon';
}
return 'internet';
}
export const backendOfflineText = nls.localize('theia/core/offline', 'Offline');
export const daemonOfflineText = nls.localize(
'theia/core/daemonOffline',
'CLI Daemon Offline'
);
export const offlineText = nls.localize('theia/core/offlineText', 'Offline');
export const backendOfflineTooltip = nls.localize(
'theia/core/cannotConnectBackend',
'Cannot connect to the backend.'
);
export const daemonOfflineTooltip = nls.localize(
'theia/core/cannotConnectDaemon',
'Cannot connect to the CLI daemon.'
);
export const offlineTooltip = offlineText;
function getOfflineText(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineText;
case 'daemon':
return '$(bolt) ' + daemonOfflineText;
case 'internet':
return '$(alert) ' + offlineText;
default:
assertUnreachable(statusType);
}
}
function getOfflineTooltip(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineTooltip;
case 'daemon':
return daemonOfflineTooltip;
case 'internet':
return offlineTooltip;
default:
assertUnreachable(statusType);
}
}

View File

@@ -10,17 +10,21 @@ import {
import * as React from '@theia/core/shared/react';
import { accountMenu } from '../../contributions/account';
import { CreateFeatures } from '../../create/create-features';
import { ApplicationConnectionStatusContribution } from './connection-status-service';
@injectable()
export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatue: ApplicationConnectionStatusContribution;
@postConstruct()
protected init(): void {
this.toDispose.push(
this.createFeatures.onDidChangeSession(() => this.update())
);
this.toDispose.pushAll([
this.createFeatures.onDidChangeSession(() => this.update()),
this.connectionStatue.onOfflineStatusDidChange(() => this.update()),
]);
}
protected override onClick(
@@ -28,7 +32,7 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
menuPath: MenuPath
): void {
const button = e.currentTarget.getBoundingClientRect();
this.contextMenuRenderer.render({
const options = {
menuPath,
includeAnchorArg: false,
anchor: {
@@ -37,7 +41,9 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
// https://github.com/eclipse-theia/theia/discussions/12170
y: button.top,
},
});
showDisabled: true,
};
this.contextMenuRenderer.render(options);
}
protected override render(): React.ReactNode {
@@ -55,7 +61,9 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
}
const arduinoAccount = menu.id === accountMenu.id;
const picture =
arduinoAccount && this.createFeatures.session?.account.picture;
arduinoAccount &&
this.connectionStatue.offlineStatus !== 'internet' &&
this.createFeatures.session?.account.picture;
const className = typeof picture === 'string' ? undefined : menu.iconClass;
return (
<i

View File

@@ -1,15 +1,19 @@
import type { Theme } from '@theia/core/lib/common/theme';
import { injectable } from '@theia/core/shared/inversify';
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
import {
BuiltinThemeProvider,
ThemeService,
} from '@theia/core/lib/browser/theming';
import { nls } from '@theia/core/lib/common/nls';
import type { Theme, ThemeType } from '@theia/core/lib/common/theme';
import { assertUnreachable } from '../../../common/utils';
export namespace ArduinoThemes {
export const Light: Theme = {
export const light: Theme = {
id: 'arduino-theme',
type: 'light',
label: 'Light (Arduino)',
editorTheme: 'arduino-theme',
};
export const Dark: Theme = {
export const dark: Theme = {
id: 'arduino-theme-dark',
type: 'dark',
label: 'Dark (Arduino)',
@@ -17,10 +21,166 @@ export namespace ArduinoThemes {
};
}
@injectable()
export class ThemeServiceWithDB extends TheiaThemeServiceWithDB {
protected override init(): void {
this.register(ArduinoThemes.Light, ArduinoThemes.Dark);
super.init();
const builtInThemeIds = new Set(
[
ArduinoThemes.light,
ArduinoThemes.dark,
BuiltinThemeProvider.hcTheme,
// TODO: add the HC light theme after Theia 1.36
].map(({ id }) => id)
);
const deprecatedThemeIds = new Set(
[BuiltinThemeProvider.lightTheme, BuiltinThemeProvider.darkTheme].map(
({ id }) => id
)
);
export const lightThemeLabel = nls.localize('arduino/theme/light', 'Light');
export const darkThemeLabel = nls.localize('arduino/theme/dark', 'Dark');
export const hcThemeLabel = nls.localize('arduino/theme/hc', 'High Contrast');
export function userThemeLabel(theme: Theme): string {
return nls.localize('arduino/theme/user', '{0} (user)', theme.label);
}
export function deprecatedThemeLabel(theme: Theme): string {
return nls.localize(
'arduino/theme/deprecated',
'{0} (deprecated)',
theme.label
);
}
export function themeLabelForSettings(theme: Theme): string {
switch (theme.id) {
case ArduinoThemes.light.id:
return lightThemeLabel;
case ArduinoThemes.dark.id:
return darkThemeLabel;
case BuiltinThemeProvider.hcTheme.id:
return hcThemeLabel;
case BuiltinThemeProvider.lightTheme.id: // fall-through
case BuiltinThemeProvider.darkTheme.id:
return deprecatedThemeLabel(theme);
default:
return userThemeLabel(theme);
}
}
export function compatibleBuiltInTheme(theme: Theme): Theme {
switch (theme.type) {
case 'light':
return ArduinoThemes.light;
case 'dark':
return ArduinoThemes.dark;
case 'hc':
return BuiltinThemeProvider.hcTheme;
default: {
console.warn(
`Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}`
);
return ArduinoThemes.light;
}
}
}
// For tests without DI
interface ThemeProvider {
themes(): Theme[];
currentTheme(): Theme;
}
/**
* Returns with a list of built-in themes officially supported by IDE2 (https://github.com/arduino/arduino-ide/issues/1283).
* The themes in the array follow the following order:
* - built-in themes first (in `Light`, `Dark`, `High Contrast`), // TODO -> High Contrast will be split up to HC Dark and HC Light after the Theia version uplift
* - followed by user installed (VSIX) themes grouped by theme type, then alphabetical order,
* - if the `currentTheme` is either Light (Theia) or Dark (Theia), the last item of the array will be the selected theme with `(deprecated)` suffix.
*/
export function userConfigurableThemes(service: ThemeService): Theme[][];
export function userConfigurableThemes(provider: ThemeProvider): Theme[][];
export function userConfigurableThemes(
serviceOrProvider: ThemeService | ThemeProvider
): Theme[][] {
const provider =
serviceOrProvider instanceof ThemeService
? {
currentTheme: () => serviceOrProvider.getCurrentTheme(),
themes: () => serviceOrProvider.getThemes(),
}
: serviceOrProvider;
const currentTheme = provider.currentTheme();
const allThemes = provider
.themes()
.map((theme) => ({ ...theme, arduinoThemeType: arduinoThemeTypeOf(theme) }))
.filter(
(theme) =>
theme.arduinoThemeType !== 'deprecated' || currentTheme.id === theme.id
)
.sort((left, right) => {
const leftArduinoThemeType = left.arduinoThemeType;
const rightArduinoThemeType = right.arduinoThemeType;
if (leftArduinoThemeType === rightArduinoThemeType) {
const result = themeTypeOrder[left.type] - themeTypeOrder[right.type];
if (result) {
return result;
}
return left.label.localeCompare(right.label); // alphabetical order
}
return (
arduinoThemeTypeOrder[leftArduinoThemeType] -
arduinoThemeTypeOrder[rightArduinoThemeType]
);
});
const builtInThemes: Theme[] = [];
const userThemes: Theme[] = [];
const deprecatedThemes: Theme[] = [];
allThemes.forEach((theme) => {
const { arduinoThemeType } = theme;
switch (arduinoThemeType) {
case 'built-in':
builtInThemes.push(theme);
break;
case 'user':
userThemes.push(theme);
break;
case 'deprecated':
deprecatedThemes.push(theme);
break;
default:
assertUnreachable(arduinoThemeType);
}
});
const groupedThemes: Theme[][] = [];
if (builtInThemes.length) {
groupedThemes.push(builtInThemes);
}
if (userThemes.length) {
groupedThemes.push(userThemes);
}
if (deprecatedThemes.length) {
groupedThemes.push(deprecatedThemes);
}
return groupedThemes;
}
export type ArduinoThemeType = 'built-in' | 'user' | 'deprecated';
const arduinoThemeTypeOrder: Record<ArduinoThemeType, number> = {
'built-in': 0,
user: 1,
deprecated: 2,
};
const themeTypeOrder: Record<ThemeType, number> = {
light: 0,
dark: 1,
hc: 2,
};
export function arduinoThemeTypeOf(theme: Theme | string): ArduinoThemeType {
const themeId = typeof theme === 'string' ? theme : theme.id;
if (builtInThemeIds.has(themeId)) {
return 'built-in';
}
if (deprecatedThemeIds.has(themeId)) {
return 'deprecated';
}
return 'user';
}

View File

@@ -1,18 +1,28 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
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 { DisposableCollection } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { isOSX } from '@theia/core/lib/common/os';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigServiceClient } from '../../config/config-service-client';
import { CreateFeatures } from '../../create/create-features';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
@injectable()
export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
@@ -22,12 +32,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
private readonly applicationShell: ApplicationShell;
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;
private _previousRepresentedFilename: string | undefined;
@inject(SketchesServiceClientImpl)
private readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConfigServiceClient)
private readonly configServiceClient: ConfigServiceClient;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(EditorManager)
private readonly editorManager: EditorManager;
private readonly applicationName =
FrontendApplicationConfigProvider.get().applicationName;
private readonly toDispose = new DisposableCollection();
private previousRepresentedFilename: string | undefined;
private applicationVersion: string | undefined;
private hasCloudPrefix: boolean | undefined;
@postConstruct()
protected init(): void {
@@ -43,6 +63,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
);
}
override onStart(app: FrontendApplication): void {
super.onStart(app);
this.toDispose.pushAll([
this.sketchesServiceClient.onCurrentSketchDidChange(() =>
this.maybeSetCloudPrefix()
),
this.configServiceClient.onDidChangeDataDirUri(() =>
this.maybeSetCloudPrefix()
),
]);
}
onStop(): void {
this.toDispose.dispose();
}
protected override handleWidgetChange(widget?: Widget | undefined): void {
if (isOSX) {
this.maybeUpdateRepresentedFilename(widget);
@@ -54,7 +90,7 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
protected override updateTitleWidget(widget?: Widget | undefined): void {
let activeEditorShort = '';
const rootName = this.workspaceService.workspace?.name ?? '';
let rootName = this.workspaceService.workspace?.name ?? '';
let appName = `${this.applicationName}${
this.applicationVersion ? ` ${this.applicationVersion}` : ''
}`;
@@ -69,6 +105,12 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
activeEditorShort = ` - ${base} `;
}
}
if (this.hasCloudPrefix) {
rootName = `[${nls.localize(
'arduino/title/cloud',
'Cloud'
)}] ${rootName}`;
}
this.windowTitleService.update({ rootName, appName, activeEditorShort });
}
@@ -77,10 +119,32 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
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) {
if (this.previousRepresentedFilename !== filename) {
const currentWindow = remote.getCurrentWindow();
currentWindow.setRepresentedFilename(uri.path.toString());
this._previousRepresentedFilename = filename;
this.previousRepresentedFilename = filename;
}
}
}
private maybeSetCloudPrefix(): void {
if (typeof this.hasCloudPrefix === 'boolean') {
return;
}
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
if (!dataDirUri) {
return;
}
this.hasCloudPrefix = this.createFeatures.isCloud(sketch, dataDirUri);
if (typeof this.hasCloudPrefix === 'boolean') {
const editor =
this.editorManager.activeEditor ?? this.editorManager.currentEditor;
if (editor) {
this.updateTitleWidget(editor);
}
}
}

View File

@@ -0,0 +1,78 @@
import { ResourceSaveOptions } from '@theia/core/lib/common/resource';
import { Readable } from '@theia/core/lib/common/stream';
import URI from '@theia/core/lib/common/uri';
import { injectable } from '@theia/core/shared/inversify';
import {
FileResource,
FileResourceOptions,
FileResourceResolver as TheiaFileResourceResolver,
} from '@theia/filesystem/lib/browser/file-resource';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import {
FileOperationError,
FileOperationResult,
FileStat,
} from '@theia/filesystem/lib/common/files';
import * as PQueue from 'p-queue';
@injectable()
export class FileResourceResolver extends TheiaFileResourceResolver {
override async resolve(uri: URI): Promise<WriteQueuedFileResource> {
let stat: FileStat | undefined;
try {
stat = await this.fileService.resolve(uri);
} catch (e) {
if (
!(
e instanceof FileOperationError &&
e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND
)
) {
throw e;
}
}
if (stat && stat.isDirectory) {
throw new Error(
'The given uri is a directory: ' + this.labelProvider.getLongName(uri)
);
}
return new WriteQueuedFileResource(uri, this.fileService, {
shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error),
});
}
}
class WriteQueuedFileResource extends FileResource {
private readonly writeQueue = new PQueue({ autoStart: true, concurrency: 1 });
constructor(
uri: URI,
fileService: FileService,
options: FileResourceOptions
) {
super(uri, fileService, options);
const originalSaveContentChanges = this['saveContentChanges'];
if (originalSaveContentChanges) {
this['saveContentChanges'] = (changes, options) => {
return this.writeQueue.add(() =>
originalSaveContentChanges.bind(this)(changes, options)
);
};
}
}
protected override async doWrite(
content: string | Readable<string>,
options?: ResourceSaveOptions
): Promise<void> {
return this.writeQueue.add(() => super.doWrite(content, options));
}
protected override async isInSync(): Promise<boolean> {
// Let all the write operations finish to update the version (mtime) before checking whether the resource is in sync.
// https://github.com/eclipse-theia/theia/issues/12327
await this.writeQueue.onIdle();
return super.isInSync();
}
}

View File

@@ -1,6 +1,5 @@
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import type {
Message,
ProgressMessage,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
@@ -46,11 +45,4 @@ export class NotificationManager extends TheiaNotificationManager {
}
return Math.min((update.work.done / update.work.total) * 100, 100);
}
/**
* For `public` visibility.
*/
override getMessageId(message: Message): string {
return super.getMessageId(message);
}
}

View File

@@ -1,23 +1,231 @@
import { injectable } from '@theia/core/shared/inversify';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { ArduinoThemes } from '../core/theming';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { ThemeService } from '@theia/core/lib/browser/theming';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import { wait } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
MonacoThemeState,
deleteTheme as deleteThemeFromIndexedDB,
getThemes as getThemesFromIndexedDB,
} from '@theia/monaco/lib/browser/monaco-indexed-db';
import {
MonacoTheme,
MonacoThemingService as TheiaMonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming';
import { WindowServiceExt } from '../core/window-service-ext';
type MonacoThemeRegistrationSource =
/**
* When reading JS/TS contributed theme from a JSON file. Such as the Arduino themes and the ones contributed by Theia.
*/
| 'compiled'
/**
* When reading and registering previous monaco themes from the `indexedDB`.
*/
| 'indexedDB'
/**
* Contributed by VS Code extensions when starting the app and loading the plugins.
*/
| 'vsix';
@injectable()
export class ThemesRegistrationSummary {
private readonly _summary: Record<MonacoThemeRegistrationSource, string[]> = {
compiled: [],
indexedDB: [],
vsix: [],
};
add(source: MonacoThemeRegistrationSource, themeId: string): void {
const themeIds = this._summary[source];
if (!themeIds.includes(themeId)) {
themeIds.push(themeId);
}
}
get summary(): Record<MonacoThemeRegistrationSource, string[]> {
return deepClone(this._summary);
}
}
@injectable()
export class MonacoThemeRegistry extends TheiaMonacoThemeRegistry {
@inject(ThemesRegistrationSummary)
private readonly summary: ThemesRegistrationSummary;
private initializing = false;
override initializeDefaultThemes(): void {
this.initializing = true;
try {
super.initializeDefaultThemes();
} finally {
this.initializing = false;
}
}
override setTheme(name: string, data: ThemeMix): void {
super.setTheme(name, data);
if (this.initializing) {
this.summary.add('compiled', name);
}
}
}
@injectable()
export class MonacoThemingService extends TheiaMonacoThemingService {
override initialize(): void {
super.initialize();
const { Light, Dark } = ArduinoThemes;
@inject(ThemesRegistrationSummary)
private readonly summary: ThemesRegistrationSummary;
private themeRegistrationSource: MonacoThemeRegistrationSource | undefined;
protected override async restore(): Promise<void> {
// The custom theme registration must happen before restoring the themes.
// Otherwise, theme changes are not picked up.
// https://github.com/arduino/arduino-ide/issues/1251#issuecomment-1436737702
this.registerArduinoThemes();
this.themeRegistrationSource = 'indexedDB';
try {
await super.restore();
} finally {
this.themeRegistrationSource = 'indexedDB';
}
}
private registerArduinoThemes(): void {
const { light, dark } = ArduinoThemes;
this.registerParsedTheme({
id: Light.id,
label: Light.label,
id: light.id,
label: light.label,
uiTheme: 'vs',
json: require('../../../../src/browser/data/default.color-theme.json'),
});
this.registerParsedTheme({
id: Dark.id,
label: Dark.label,
id: dark.id,
label: dark.label,
uiTheme: 'vs-dark',
json: require('../../../../src/browser/data/dark.color-theme.json'),
});
}
protected override doRegisterParsedTheme(
state: MonacoThemeState
): Disposable {
const themeId = state.id;
const source = this.themeRegistrationSource ?? 'compiled';
const disposable = super.doRegisterParsedTheme(state);
this.summary.add(source, themeId);
return disposable;
}
protected override async doRegister(
theme: MonacoTheme,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pending: { [uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<void> {
try {
this.themeRegistrationSource = 'vsix';
await super.doRegister(theme, pending, toDispose);
} finally {
this.themeRegistrationSource = undefined;
}
}
}
/**
* Workaround for removing VSIX themes from the indexedDB if they were not loaded during the app startup.
*/
@injectable()
export class CleanupObsoleteThemes implements FrontendApplicationContribution {
@inject(HostedPluginSupport)
private readonly hostedPlugin: HostedPluginSupport;
@inject(ThemesRegistrationSummary)
private readonly summary: ThemesRegistrationSummary;
@inject(ThemeService)
private readonly themeService: ThemeService;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(WindowServiceExt)
private readonly windowService: WindowServiceExt;
onStart(): void {
this.hostedPlugin.didStart.then(() => this.cleanupObsoleteThemes());
}
private async cleanupObsoleteThemes(): Promise<void> {
const persistedThemes = await getThemesFromIndexedDB();
const obsoleteThemeIds = collectObsoleteThemeIds(
persistedThemes,
this.summary.summary
);
if (!obsoleteThemeIds.length) {
return;
}
const firstWindow = await this.windowService.isFirstWindow();
if (firstWindow) {
await this.removeObsoleteThemesFromIndexedDB(obsoleteThemeIds);
this.unregisterObsoleteThemes(obsoleteThemeIds);
}
}
private removeObsoleteThemesFromIndexedDB(themeIds: string[]): Promise<void> {
return themeIds.reduce(async (previousTask, themeId) => {
await previousTask;
return deleteThemeFromIndexedDB(themeId);
}, Promise.resolve());
}
private unregisterObsoleteThemes(themeIds: string[]): void {
const currentTheme = this.themeService.getCurrentTheme();
const switchToCompatibleTheme = themeIds.includes(currentTheme.id);
for (const themeId of themeIds) {
delete this.themeService['themes'][themeId];
}
this.themeService['doUpdateColorThemePreference']();
if (switchToCompatibleTheme) {
this.themeService.setCurrentTheme(
compatibleBuiltInTheme(currentTheme).id,
true
);
wait(250).then(() =>
requestAnimationFrame(() =>
this.messageService.info(
nls.localize(
'arduino/theme/currentThemeNotFound',
'Could not find the currently selected theme: {0}. Arduino IDE has picked a built-in theme compatible with the missing one.',
currentTheme.label
)
)
)
);
}
}
}
/**
* An indexedDB registered theme is obsolete if it is in the indexedDB but was registered
* from neither a `vsix` nor `compiled` source during the app startup.
*/
export function collectObsoleteThemeIds(
indexedDBThemes: MonacoThemeState[],
summary: Record<MonacoThemeRegistrationSource, string[]>
): string[] {
const vsixThemeIds = summary['vsix'];
const compiledThemeIds = summary['compiled'];
return indexedDBThemes
.map(({ id }) => id)
.filter(
(id) => !vsixThemeIds.includes(id) && !compiledThemeIds.includes(id)
);
}

View File

@@ -0,0 +1,37 @@
import { notEmpty } from '@theia/core';
/**
* Finds the closest child HTMLButtonElement representing a Theia button.
* A button is a Theia button if it's a `<button>` element and has the `"theia-button"` class.
* If an element has multiple Theia button children, this function prefers `"main"` over `"secondary"` button.
*/
export function findChildTheiaButton(
element: HTMLElement,
recursive = false
): HTMLButtonElement | undefined {
let button: HTMLButtonElement | undefined = undefined;
const children = Array.from(element.children);
for (const child of children) {
if (
child instanceof HTMLButtonElement &&
child.classList.contains('theia-button')
) {
if (child.classList.contains('main')) {
return child;
}
button = child;
}
}
if (!button && recursive) {
button = children
.filter(isHTMLElement)
.map((childElement) => findChildTheiaButton(childElement, true))
.filter(notEmpty)
.shift();
}
return button;
}
function isHTMLElement(element: Element): element is HTMLElement {
return element instanceof HTMLElement;
}

View File

@@ -1,12 +1,13 @@
import { FileStat } from '@theia/filesystem/lib/common/files';
import { injectable } from '@theia/core/shared/inversify';
import { toPosixPath } from '../../create/create-paths';
import { splitSketchPath } from '../../create/create-paths';
import { Create } from '../../create/typings';
@injectable()
export class SketchCache {
sketches: Record<string, Create.Sketch> = {};
fileStats: Record<string, FileStat> = {};
private _createPathPrefix: string | undefined;
init(): void {
// reset the data
@@ -32,7 +33,10 @@ export class SketchCache {
addSketch(sketch: Create.Sketch): void {
const { path } = sketch;
const posixPath = toPosixPath(path);
const [pathPrefix, posixPath] = splitSketchPath(path);
if (pathPrefix !== this._createPathPrefix) {
this._createPathPrefix = pathPrefix;
}
this.sketches[posixPath] = sketch;
}
@@ -40,6 +44,10 @@ export class SketchCache {
return this.sketches[path] || null;
}
get createPathPrefix(): string | undefined {
return this._createPathPrefix;
}
toString(): string {
return JSON.stringify({
sketches: this.sketches,

View File

@@ -0,0 +1,88 @@
import { TreeNode } from '@theia/core/lib/browser/tree';
import { Command } from '@theia/core/lib/common/command';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
export namespace CloudSketchbookCommands {
export interface Arg {
model: CloudSketchbookTreeModel;
node: TreeNode;
event?: MouseEvent;
}
export namespace Arg {
export function is(arg: unknown): arg is Arg {
return (
typeof arg === 'object' &&
(<Arg>arg).model !== undefined &&
(<Arg>arg).model instanceof CloudSketchbookTreeModel &&
(<Arg>arg).node !== undefined &&
TreeNode.is((<Arg>arg).node)
);
}
}
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--disable',
label: 'Show/Hide Cloud Sketchbook',
},
'arduino/cloud/showHideSketchbook'
);
export const PULL_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--pull-sketch',
label: 'Pull Sketch',
iconClass: 'fa fa-arduino-cloud-download',
},
'arduino/cloud/pullSketch'
);
export const PUSH_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--push-sketch',
label: 'Push Sketch',
iconClass: 'fa fa-arduino-cloud-upload',
},
'arduino/cloud/pullSketch'
);
export const PULL_SKETCH__TOOLBAR = {
...PULL_SKETCH,
id: `${PULL_SKETCH.id}-toolbar`,
};
export const PUSH_SKETCH__TOOLBAR = {
...PUSH_SKETCH,
id: `${PUSH_SKETCH.id}-toolbar`,
};
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
label: 'Open in Cloud Editor',
},
'arduino/cloud/openInCloudEditor'
);
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
{
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
label: 'Options...',
iconClass: 'sketchbook-tree__opts',
},
'arduino/cloud/options'
);
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--share-modal',
label: 'Share...',
},
'arduino/cloud/share'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}

View File

@@ -5,7 +5,7 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { CloudStatus } from './cloud-user-status';
import { CloudStatus } from './cloud-status';
import { nls } from '@theia/core/lib/common/nls';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
@@ -13,6 +13,7 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
import { CreateNew } from '../sketchbook/create-new';
import { AuthenticationSession } from '../../../node/auth/types';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
@injectable()
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {
@@ -20,6 +21,9 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
private readonly authenticationService: AuthenticationClientService;
@inject(CloudSketchbookTreeWidget)
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private _session: AuthenticationSession | undefined;
constructor() {
@@ -66,6 +70,7 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
}
authenticationService={this.authenticationService}
connectionStatus={this.connectionStatus}
/>
</>
);

View File

@@ -1,145 +1,94 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import {
ContextMenuRenderer,
RenderContextMenuOptions,
} from '@theia/core/lib/browser';
} from '@theia/core/lib/browser/context-menu-renderer';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
import { CreateApi } from '../../create/create-api';
import {
PreferenceService,
PreferenceScope,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
import { Contribution } from '../../contributions/contribution';
import { nls } from '@theia/core/lib/common/nls';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArduinoPreferences } from '../../arduino-preferences';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { nls } from '@theia/core/lib/common';
import { ConfigServiceClient } from '../../config/config-service-client';
import { CloudSketchContribution } from '../../contributions/cloud-contribution';
import {
Sketch,
TabBarToolbarRegistry,
} from '../../contributions/contribution';
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
import { CurrentSketch } from '../../sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CreateUri } from '../../create/create-uri';
export const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
// `Open Folder`, `Open in New Window`
export const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
...SKETCHBOOKSYNC__CONTEXT,
'0_main',
];
export namespace CloudSketchbookCommands {
export interface Arg {
model: CloudSketchbookTreeModel;
node: TreeNode;
event?: MouseEvent;
}
export namespace Arg {
export function is(arg: Partial<Arg> | undefined): arg is Arg {
return (
!!arg && !!arg.node && arg.model instanceof CloudSketchbookTreeModel
);
}
}
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--disable',
label: 'Show/Hide Cloud Sketchbook',
},
'arduino/cloud/showHideSketchbook'
);
export const PULL_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--pull-sketch',
label: 'Pull Sketch',
iconClass: 'pull-sketch-icon',
},
'arduino/cloud/pullSketch'
);
export const PUSH_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--push-sketch',
label: 'Push Sketch',
iconClass: 'push-sketch-icon',
},
'arduino/cloud/pullSketch'
);
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
label: 'Open in Cloud Editor',
},
'arduino/cloud/openInCloudEditor'
);
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
{
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
label: 'Options...',
iconClass: 'sketchbook-tree__opts',
},
'arduino/cloud/options'
);
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--share-modal',
label: 'Share...',
},
'arduino/cloud/share'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}
@injectable()
export class CloudSketchbookContribution extends Contribution {
@inject(FileService)
protected readonly fileService: FileService;
export class CloudSketchbookContribution extends CloudSketchContribution {
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
private readonly contextMenuRenderer: ContextMenuRenderer;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
private readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
private readonly windowService: WindowService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
private readonly preferenceService: PreferenceService;
@inject(ConfigServiceClient)
private readonly configServiceClient: ConfigServiceClient;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
private readonly onDidChangeToolbarEmitter = new Emitter<void>();
private readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
private readonly toDisposeOnStop = new DisposableCollection(
this.onDidChangeToolbarEmitter,
this.toDisposeBeforeNewContextMenu
);
private shell: ApplicationShell | undefined;
protected readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
this.toDisposeOnStop.pushAll([
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
if (!offlineStatus || offlineStatus === 'internet') {
this.fireToolbarChange();
}
}),
this.createFeatures.onDidChangeSession(() => this.fireToolbarChange()),
this.createFeatures.onDidChangeEnabled(() => this.fireToolbarChange()),
this.createFeatures.onDidChangeCloudSketchState(() =>
this.fireToolbarChange()
),
]);
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
override registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, {
@@ -149,6 +98,23 @@ export class CloudSketchbookContribution extends Contribution {
});
}
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
command: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
tooltip: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.label,
priority: -2,
onDidChange: this.onDidChangeToolbar,
});
registry.registerItem({
id: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
command: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
tooltip: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.label,
priority: -1,
onDidChange: this.onDidChangeToolbar,
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK, {
execute: () => {
@@ -158,32 +124,41 @@ export class CloudSketchbookContribution extends Contribution {
PreferenceScope.User
);
},
isEnabled: () => true,
isVisible: () => true,
});
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH, {
execute: (arg) => arg.model.sketchbookTree().pull(arg),
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH, {
execute: (arg) => arg.model.sketchbookTree().push(arg.node),
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
this.isCloudSketchDirNodeCommandArg(arg) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
this.isCloudSketchDirNodeCommandArg(arg) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
});
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR, {
execute: () =>
this.executeDelegateWithCurrentSketch(
CloudSketchbookCommands.PUSH_SKETCH.id
),
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
});
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH__TOOLBAR, {
execute: () =>
this.executeDelegateWithCurrentSketch(
CloudSketchbookCommands.PULL_SKETCH.id
),
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
});
registry.registerCommand(CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR, {
execute: (arg) => {
this.windowService.openNewWindow(
@@ -191,12 +166,8 @@ export class CloudSketchbookContribution extends Contribution {
{ external: true }
);
},
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG, {
@@ -207,12 +178,8 @@ export class CloudSketchbookContribution extends Contribution {
createApi: this.createApi,
}).open();
},
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(
@@ -316,7 +283,118 @@ export class CloudSketchbookContribution extends Contribution {
},
}
);
}
this.registerMenus(this.menuRegistry);
private get currentCloudSketch(): Sketch | undefined {
const currentSketch = this.sketchServiceClient.tryGetCurrentSketch();
// could not load sketch via CLI
if (!CurrentSketch.isValid(currentSketch)) {
return undefined;
}
// cannot determine if the sketch is in the cloud cache folder
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
if (!dataDirUri) {
return undefined;
}
// sketch is not in the cache folder
if (!this.createFeatures.isCloud(currentSketch, dataDirUri)) {
return undefined;
}
return currentSketch;
}
private isVisibleCloudSketchToolbar(arg: unknown): boolean {
// cloud preference is disabled
if (!this.createFeatures.enabled) {
return false;
}
if (!this.currentCloudSketch) {
return false;
}
if (arg instanceof Widget) {
return !!this.shell && this.shell.getWidgets('main').indexOf(arg) !== -1;
}
return false;
}
private isEnabledCloudSketchToolbar(arg: unknown): boolean {
if (!this.isVisibleCloudSketchToolbar(arg)) {
return false;
}
// not logged in
if (!this.createFeatures.session) {
return false;
}
// no Internet connection
if (this.connectionStatus.offlineStatus === 'internet') {
return false;
}
// no pull/push context for the current cloud sketch
const sketch = this.currentCloudSketch;
if (sketch) {
const cloudUri = this.createFeatures.cloudUri(sketch);
if (cloudUri) {
return !this.createFeatures.cloudSketchState(
CreateUri.toUri(cloudUri.path.toString())
);
}
}
return false;
}
private isCloudSketchDirNodeCommandArg(
arg: unknown
): arg is CloudSketchbookCommands.Arg & {
node: CloudSketchbookTree.CloudSketchDirNode;
} {
return (
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!this.createFeatures.cloudSketchState(arg.node.remoteUri)
);
}
private async commandArgFromCurrentSketch(): Promise<
CloudSketchbookCommands.Arg | undefined
> {
const sketch = this.currentCloudSketch;
if (!sketch) {
return undefined;
}
const model = await this.treeModel();
if (!model) {
return undefined;
}
const cloudUri = this.createFeatures.cloudUri(sketch);
if (!cloudUri) {
return undefined;
}
const posixPath = cloudUri.path.toString();
const node = model.getNode(posixPath);
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
return { model, node };
}
return undefined;
}
private async executeDelegateWithCurrentSketch(id: string): Promise<unknown> {
const arg = await this.commandArgFromCurrentSketch();
if (!arg) {
return;
}
if (!this.commandRegistry.getActiveHandler(id, arg)) {
throw new Error(
`No active handler was available for the delegate command: ${id}. Cloud sketch tree node: ${arg.node.id}`
);
}
return this.commandRegistry.executeCommand(id, arg);
}
private fireToolbarChange(): void {
this.onDidChangeToolbarEmitter.fire();
}
private get onDidChangeToolbar(): Event<void> {
return this.onDidChangeToolbarEmitter.event;
}
}

View File

@@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri';
import { Create } from '../../create/typings';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
function sketchBaseDir(sketch: Create.Sketch): FileStat {
// extract the sketch path
@@ -63,15 +64,22 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private _localCacheFsProviderReady: Deferred<void> | undefined;
@postConstruct()
protected override init(): void {
super.init();
this.toDispose.push(
this.authenticationService.onSessionDidChange(() => this.updateRoot())
);
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange(() => this.updateRoot()),
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
if (!offlineStatus) {
this.updateRoot();
}
}),
]);
}
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {

View File

@@ -15,6 +15,7 @@ import { CompositeTreeNode } from '@theia/core/lib/browser';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
@injectable()
export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@@ -27,6 +28,9 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: CloudSketchbookTree;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
protected override renderTree(model: TreeModel): React.ReactNode {
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
if (this.shouldShowEmptyView()) return this.renderEmptyView();
@@ -91,10 +95,33 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
return classNames;
}
protected override renderIcon(
node: TreeNode,
props: NodeProps
): React.ReactNode {
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
const synced = CloudSketchbookTree.CloudSketchTreeNode.isSynced(node);
const offline = this.connectionStatus.offlineStatus === 'internet';
const icon = `fa fa-arduino-cloud${synced ? '-filled' : ''}${
offline ? '-offline' : ''
}`;
return (
<div
className={`theia-file-icons-js file-icon${
!synced && offline ? ' not-in-sync-offline' : ''
}`}
>
<div className={icon} />
</div>
);
}
return super.renderIcon(node, props);
}
protected override renderInlineCommands(node: any): React.ReactNode {
if (CloudSketchbookTree.CloudSketchDirNode.is(node) && node.commands) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node, {
this.renderInlineCommand(command, node, {
username: this.authenticationService.session?.account?.label,
})
);

View File

@@ -22,15 +22,22 @@ import {
LocalCacheFsProvider,
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { firstToUpperCase } from '../../../common/utils';
import { assertUnreachable } from '../../../common/utils';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
import { posix, splitSketchPath } from '../../create/create-paths';
import { Create } from '../../create/typings';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
import { ExecuteWithProgress } from '../../../common/protocol/progressible';
import {
pullingSketch,
pushingSketch,
} from '../../contributions/cloud-contribution';
import { CloudSketchState, CreateFeatures } from '../../create/create-features';
const MESSAGE_TIMEOUT = 5 * 1000;
const deepmerge = require('deepmerge').default;
@@ -54,6 +61,19 @@ export class CloudSketchbookTree extends SketchbookTree {
@inject(CreateApi)
private readonly createApi: CreateApi;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
protected override init(): void {
this.toDispose.push(
this.connectionStatus.onOfflineStatusDidChange(() => this.refresh())
);
super.init();
}
async pushPublicWarn(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<boolean> {
@@ -84,7 +104,7 @@ export class CloudSketchbookTree extends SketchbookTree {
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
async pull(arg: any): Promise<void> {
async pull(arg: any, noProgress = false): Promise<void> {
const {
// model,
node,
@@ -118,32 +138,45 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
}
return this.runWithState(node, 'pulling', async (node) => {
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(node.remoteUri, localUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePulling',
'Done pulling {0}.',
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
});
return this.runWithState(
node,
'pull',
async (node) => {
await this.pullNode(node);
},
noProgress
);
}
async push(node: CloudSketchbookTree.CloudSketchDirNode): Promise<void> {
private async pullNode(node: CloudSketchbookTree.CloudSketchDirNode) {
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(node.remoteUri, localUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePulling',
"Done pulling '{0}'.",
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
}
async push(
node: CloudSketchbookTree.CloudSketchDirNode,
noProgress = false,
ignorePushWarnings = false
): Promise<void> {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
@@ -158,7 +191,8 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
const warn = this.arduinoPreferences['arduino.cloud.push.warn'];
const warn =
!ignorePushWarnings && this.arduinoPreferences['arduino.cloud.push.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
@@ -178,37 +212,46 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
}
return this.runWithState(node, 'pushing', async (node) => {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
'arduino/cloud/pullFirst',
'You have to pull first to be able to push to the Cloud.'
)
);
}
const commandsCopy = node.commands;
node.commands = [];
return this.runWithState(
node,
'push',
async (node) => {
await this.pushNode(node);
},
noProgress
);
}
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(localUri, node.remoteUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
private async pushNode(node: CloudSketchbookTree.CloudSketchDirNode) {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
'arduino/cloud/donePushing',
'Done pushing {0}.',
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
'arduino/cloud/pullFirst',
'You have to pull first to be able to push to the Cloud.'
)
);
});
}
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(localUri, node.remoteUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePushing',
"Done pushing '{0}'.",
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
}
private async recursiveURIs(uri: URI): Promise<URI[]> {
@@ -310,31 +353,37 @@ export class CloudSketchbookTree extends SketchbookTree {
private async runWithState<T>(
node: CloudSketchbookTree.CloudSketchDirNode & Partial<DecoratedTreeNode>,
state: CloudSketchbookTree.CloudSketchDirNode.State,
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>
state: CloudSketchState,
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>,
noProgress = false
): Promise<T> {
const decoration: WidgetDecoration.TailDecoration = {
data: `${firstToUpperCase(state)}...`,
fontData: {
color: 'var(--theia-list-highlightForeground)',
},
};
this.createFeatures.setCloudSketchState(node.remoteUri, state);
try {
node.state = state;
this.mergeDecoration(node, { tailDecorations: [decoration] });
const result = await (noProgress
? task(node)
: ExecuteWithProgress.withProgress(
this.taskMessage(state, node.uri.path.name),
this.messageService,
async (progress) => {
progress.report({ work: { done: 0, total: NaN } });
return task(node);
}
));
await this.refresh(node);
const result = await task(node);
return result;
} finally {
delete node.state;
// TODO: find a better way to attach and detach decorators. Do we need a proper `TreeDecorator` instead?
const index = node.decorationData?.tailDecorations?.findIndex(
(candidate) => JSON.stringify(decoration) === JSON.stringify(candidate)
);
if (typeof index === 'number' && index !== -1) {
node.decorationData?.tailDecorations?.splice(index, 1);
}
await this.refresh(node);
this.createFeatures.setCloudSketchState(node.remoteUri, undefined);
}
}
private taskMessage(state: CloudSketchState, input: string): string {
switch (state) {
case 'pull':
return pullingSketch(input);
case 'push':
return pushingSketch(input);
default:
assertUnreachable(state);
}
}
@@ -501,7 +550,7 @@ export class CloudSketchbookTree extends SketchbookTree {
};
}
protected readonly notInSyncDecoration: WidgetDecoration.Data = {
protected readonly notInSyncOfflineDecoration: WidgetDecoration.Data = {
fontData: {
color: 'var(--theia-activityBar-inactiveForeground)',
},
@@ -522,11 +571,15 @@ export class CloudSketchbookTree extends SketchbookTree {
node.fileStat.resource.path.toString()
);
const commands = [CloudSketchbookCommands.PULL_SKETCH];
const commands: Command[] = [];
if (this.connectionStatus.offlineStatus !== 'internet') {
commands.push(CloudSketchbookCommands.PULL_SKETCH);
}
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
this.connectionStatus.offlineStatus !== 'internet'
) {
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
}
@@ -557,14 +610,15 @@ export class CloudSketchbookTree extends SketchbookTree {
}
}
// add style decoration for not-in-sync files
// add style decoration for not-in-sync files when offline
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
this.connectionStatus.offlineStatus === 'internet'
) {
this.mergeDecoration(node, this.notInSyncDecoration);
this.mergeDecoration(node, this.notInSyncOfflineDecoration);
} else {
this.removeDecoration(node, this.notInSyncDecoration);
this.removeDecoration(node, this.notInSyncOfflineDecoration);
}
return node;
@@ -644,7 +698,7 @@ export namespace CloudSketchbookTree {
export interface CloudSketchDirNode
extends Omit<SketchbookTree.SketchDirNode, 'fileStat'>,
CloudSketchTreeNode {
state?: CloudSketchDirNode.State;
state?: CloudSketchState;
isPublic?: boolean;
sketchId?: string;
commands?: Command[];
@@ -653,7 +707,5 @@ export namespace CloudSketchbookTree {
export function is(node: TreeNode | undefined): node is CloudSketchDirNode {
return SketchbookTree.SketchDirNode.is(node);
}
export type State = 'syncing' | 'pulling' | 'pushing';
}
}

View File

@@ -1,19 +1,17 @@
import * as React from '@theia/core/shared/react';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
export class CloudStatus extends React.Component<
UserStatus.Props,
UserStatus.State
CloudStatus.Props,
CloudStatus.State
> {
protected readonly toDispose = new DisposableCollection();
constructor(props: UserStatus.Props) {
constructor(props: CloudStatus.Props) {
super(props);
this.state = {
status: this.status,
@@ -22,17 +20,11 @@ export class CloudStatus extends React.Component<
}
override componentDidMount(): void {
const statusListener = () => this.setState({ status: this.status });
window.addEventListener('online', statusListener);
window.addEventListener('offline', statusListener);
this.toDispose.pushAll([
Disposable.create(() =>
window.removeEventListener('online', statusListener)
),
Disposable.create(() =>
window.removeEventListener('offline', statusListener)
),
]);
this.toDispose.push(
this.props.connectionStatus.onOfflineStatusDidChange(() =>
this.setState({ status: this.status })
)
);
}
override componentWillUnmount(): void {
@@ -58,14 +50,21 @@ export class CloudStatus extends React.Component<
: nls.localize('arduino/cloud/offline', 'Offline')}
</div>
<div className="actions item flex-line">
<div
title={nls.localize('arduino/cloud/sync', 'Sync')}
className={`fa fa-reload ${
(this.state.refreshing && 'rotating') || ''
}`}
style={{ cursor: 'pointer' }}
onClick={this.onDidClickRefresh}
/>
{this.props.connectionStatus.offlineStatus === 'internet' ? (
<div
className="fa fa-arduino-cloud-offline"
title={nls.localize('arduino/cloud/offline', 'Offline')}
/>
) : (
<div
title={nls.localize('arduino/cloud/sync', 'Sync')}
className={`fa fa-reload ${
(this.state.refreshing && 'rotating') || ''
}`}
style={{ cursor: 'pointer' }}
onClick={this.onDidClickRefresh}
/>
)}
</div>
</div>
);
@@ -83,14 +82,17 @@ export class CloudStatus extends React.Component<
};
private get status(): 'connected' | 'offline' {
return window.navigator.onLine ? 'connected' : 'offline';
return this.props.connectionStatus.offlineStatus === 'internet'
? 'offline'
: 'connected';
}
}
export namespace UserStatus {
export namespace CloudStatus {
export interface Props {
readonly model: CloudSketchbookTreeModel;
readonly authenticationService: AuthenticationClientService;
readonly connectionStatus: ApplicationConnectionStatusContribution;
}
export interface State {
status: 'connected' | 'offline';

View File

@@ -1,60 +1,76 @@
import * as React from '@theia/core/shared/react';
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
import type { ListItemRenderer } from './list-item-renderer';
import { UserAbortError } from './list-widget';
export class ComponentListItem<
T extends ArduinoComponent
> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
constructor(props: ComponentListItem.Props<T>) {
super(props);
if (props.item.installable) {
const version = props.item.availableVersions.filter(
(version) => version !== props.item.installedVersion
)[0];
this.state = {
selectedVersion: version,
};
}
this.state = {};
}
override render(): React.ReactNode {
const { item, itemRenderer } = this.props;
const selectedVersion =
this.props.edited?.item.name === item.name
? this.props.edited.selectedVersion
: this.latestVersion;
return (
<>
{itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
)}
{itemRenderer.renderItem({
item,
selectedVersion,
inProgress: this.state.inProgress,
install: (item) => this.install(item),
uninstall: (item) => this.uninstall(item),
onVersionChange: (version) => this.onVersionChange(version),
})}
</>
);
}
private async install(item: T): Promise<void> {
const toInstall = this.state.selectedVersion;
const version = this.props.item.availableVersions.filter(
(version) => version !== this.state.selectedVersion
)[0];
this.setState({
selectedVersion: version,
});
try {
await this.props.install(item, toInstall);
} catch {
this.setState({
selectedVersion: toInstall,
});
}
await this.withState('installing', () =>
this.props.install(
item,
this.props.edited?.item.name === item.name
? this.props.edited.selectedVersion
: Installable.latest(this.props.item.availableVersions)
)
);
}
private async uninstall(item: T): Promise<void> {
await this.props.uninstall(item);
await this.withState('uninstalling', () => this.props.uninstall(item));
}
private async withState(
inProgress: 'installing' | 'uninstalling',
task: () => Promise<unknown>
): Promise<void> {
this.setState({ inProgress });
try {
await task();
} catch (err) {
if (err instanceof UserAbortError) {
// No state update when user cancels the task
return;
}
throw err;
} finally {
this.setState({ inProgress: undefined });
}
}
private onVersionChange(version: Installable.Version): void {
this.setState({ selectedVersion: version });
this.props.onItemEdit(this.props.item, version);
}
private get latestVersion(): Installable.Version | undefined {
return Installable.latest(this.props.item.availableVersions);
}
}
@@ -63,10 +79,18 @@ export namespace ComponentListItem {
readonly item: T;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly edited?: {
item: T;
selectedVersion: Installable.Version;
};
readonly onItemEdit: (
item: T,
selectedVersion: Installable.Version
) => void;
readonly itemRenderer: ListItemRenderer<T>;
}
export interface State {
selectedVersion?: Installable.Version;
inProgress?: 'installing' | 'uninstalling' | undefined;
}
}

View File

@@ -1,148 +1,32 @@
import 'react-virtualized/styles.css';
import * as React from '@theia/core/shared/react';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import {
CellMeasurer,
CellMeasurerCache,
} from 'react-virtualized/dist/commonjs/CellMeasurer';
import type {
ListRowProps,
ListRowRenderer,
} from 'react-virtualized/dist/commonjs/List';
import List from 'react-virtualized/dist/commonjs/List';
import { Virtuoso } from '@theia/core/shared/react-virtuoso';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable';
import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer';
function sameAs<T>(
left: T[],
right: T[],
...compareProps: (keyof T)[]
): boolean {
if (left === right) {
return true;
}
const leftLength = left.length;
if (leftLength !== right.length) {
return false;
}
for (let i = 0; i < leftLength; i++) {
for (const prop of compareProps) {
const leftValue = left[i][prop];
const rightValue = right[i][prop];
if (leftValue !== rightValue) {
return false;
}
}
}
return true;
}
export class ComponentList<T extends ArduinoComponent> extends React.Component<
ComponentList.Props<T>
> {
private readonly cache: CellMeasurerCache;
private resizeAllFlag: boolean;
private list: List | undefined;
private mostRecentWidth: number | undefined;
constructor(props: ComponentList.Props<T>) {
super(props);
this.cache = new CellMeasurerCache({
defaultHeight: 140,
fixedWidth: true,
});
}
override render(): React.ReactNode {
return (
<AutoSizer>
{({ width, height }) => {
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
this.resizeAllFlag = true;
setTimeout(() => this.clearAll(), 0);
}
this.mostRecentWidth = width;
return (
<List
className={'items-container'}
rowRenderer={this.createItem}
height={height}
width={width}
rowCount={this.props.items.length}
rowHeight={this.cache.rowHeight}
deferredMeasurementCache={this.cache}
ref={this.setListRef}
estimatedRowSize={140}
// If default value, then `react-virtualized` will optimize and list item will not receive a `:hover` event.
// Hence, install and version `<select>` won't be visible even if the mouse cursor is over the `<div>`.
// See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42
scrollingResetTimeInterval={0}
/>
);
}}
</AutoSizer>
);
}
override componentDidUpdate(prevProps: ComponentList.Props<T>): void {
if (
this.resizeAllFlag ||
!sameAs(this.props.items, prevProps.items, 'name', 'installedVersion')
) {
this.clearAll(true);
}
}
private readonly setListRef = (ref: List | null): void => {
this.list = ref || undefined;
};
private clearAll(scrollToTop = false): void {
this.resizeAllFlag = false;
this.cache.clearAll();
if (this.list) {
this.list.recomputeRowHeights();
if (scrollToTop) {
this.list.scrollToPosition(0);
}
}
}
private readonly createItem: ListRowRenderer = ({
index,
parent,
key,
style,
}: ListRowProps): React.ReactNode => {
const item = this.props.items[index];
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
rowIndex={index}
parent={parent}
>
{({ registerChild }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<div ref={registerChild} style={style}>
<ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall}
/>
</div>
<Virtuoso
data={this.props.items}
itemContent={(_: number, item: T) => (
<ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall}
edited={this.props.edited}
onItemEdit={this.props.onItemEdit}
/>
)}
</CellMeasurer>
/>
);
};
}
}
export namespace ComponentList {
export interface Props<T extends ArduinoComponent> {
readonly items: T[];
@@ -150,5 +34,13 @@ export namespace ComponentList {
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly edited?: {
item: T;
selectedVersion: Installable.Version;
};
readonly onItemEdit: (
item: T,
selectedVersion: Installable.Version
) => void;
}
}

View File

@@ -15,6 +15,7 @@ import { ListItemRenderer } from './list-item-renderer';
import { ResponseServiceClient } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { FilterRenderer } from './filter-renderer';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
export class FilterableListContainer<
T extends ArduinoComponent,
@@ -23,21 +24,30 @@ export class FilterableListContainer<
FilterableListContainer.Props<T, S>,
FilterableListContainer.State<T, S>
> {
private readonly toDispose: DisposableCollection;
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
super(props);
this.state = {
searchOptions: props.defaultSearchOptions,
items: [],
};
this.toDispose = new DisposableCollection();
}
override componentDidMount(): void {
this.search = debounce(this.search, 500, { trailing: true });
this.search(this.state.searchOptions);
this.props.searchOptionsDidChange((newSearchOptions) => {
const { searchOptions } = this.state;
this.setSearchOptionsAndUpdate({ ...searchOptions, ...newSearchOptions });
});
this.toDispose.pushAll([
this.props.searchOptionsDidChange((newSearchOptions) => {
const { searchOptions } = this.state;
this.setSearchOptionsAndUpdate({
...searchOptions,
...newSearchOptions,
});
}),
this.props.onDidShow(() => this.setState({ edited: undefined })),
]);
}
override componentDidUpdate(): void {
@@ -46,6 +56,10 @@ export class FilterableListContainer<
this.props.container.updateScrollBar();
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return (
<div className={'filterable-list-container'}>
@@ -90,11 +104,13 @@ export class FilterableListContainer<
itemRenderer={itemRenderer}
install={this.install.bind(this)}
uninstall={this.uninstall.bind(this)}
edited={this.state.edited}
onItemEdit={this.onItemEdit.bind(this)}
/>
);
}
protected handlePropChange = (prop: keyof S, value: S[keyof S]): void => {
private handlePropChange = (prop: keyof S, value: S[keyof S]): void => {
const searchOptions = {
...this.state.searchOptions,
[prop]: value,
@@ -106,15 +122,14 @@ export class FilterableListContainer<
this.setState({ searchOptions }, () => this.search(searchOptions));
}
protected search(searchOptions: S): void {
private search(searchOptions: S): void {
const { searchable } = this.props;
searchable.search(searchOptions).then((items) => this.setState({ items }));
searchable
.search(searchOptions)
.then((items) => this.setState({ items, edited: undefined }));
}
protected async install(
item: T,
version: Installable.Version
): Promise<void> {
private async install(item: T, version: Installable.Version): Promise<void> {
const { install, searchable } = this.props;
await ExecuteWithProgress.doWithProgress({
...this.props,
@@ -124,10 +139,10 @@ export class FilterableListContainer<
run: ({ progressId }) => install({ item, progressId, version }),
});
const items = await searchable.search(this.state.searchOptions);
this.setState({ items });
this.setState({ items, edited: undefined });
}
protected async uninstall(item: T): Promise<void> {
private async uninstall(item: T): Promise<void> {
const ok = await new ConfirmDialog({
title: nls.localize('arduino/component/uninstall', 'Uninstall'),
msg: nls.localize(
@@ -152,7 +167,11 @@ export class FilterableListContainer<
run: ({ progressId }) => uninstall({ item, progressId }),
});
const items = await searchable.search(this.state.searchOptions);
this.setState({ items });
this.setState({ items, edited: undefined });
}
private onItemEdit(item: T, selectedVersion: Installable.Version): void {
this.setState({ edited: { item, selectedVersion } });
}
}
@@ -171,6 +190,7 @@ export namespace FilterableListContainer {
readonly searchOptionsDidChange: Event<Partial<S> | undefined>;
readonly messageService: MessageService;
readonly responseService: ResponseServiceClient;
readonly onDidShow: Event<void>;
readonly install: ({
item,
progressId,
@@ -193,5 +213,9 @@ export namespace FilterableListContainer {
export interface State<T, S extends Searchable.Options> {
searchOptions: S;
items: T[];
edited?: {
item: T;
selectedVersion: Installable.Version;
};
}
}

View File

@@ -1,137 +1,783 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationError } from '@theia/core';
import {
Anchor,
ContextMenuRenderer,
} from '@theia/core/lib/browser/context-menu-renderer';
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { codicon } from '@theia/core/lib/browser/widgets/widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
import { nls } from '@theia/core/lib/common';
import {
CommandHandler,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import {
MenuModelRegistry,
MenuPath,
SubMenuOptions,
} from '@theia/core/lib/common/menu';
import { MessageService } from '@theia/core/lib/common/message-service';
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 { Unknown } from '../../../common/nls';
import {
CoreService,
ExamplesService,
LibraryPackage,
Sketch,
SketchContainer,
SketchesService,
SketchRef,
} from '../../../common/protocol';
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable';
import { openClonedExample } from '../../contributions/examples';
import {
ArduinoMenus,
examplesLabel,
unregisterSubmenu,
} from '../../menu/arduino-menus';
const moreInfoLabel = nls.localize('arduino/component/moreInfo', 'More info');
const otherVersionsLabel = nls.localize(
'arduino/component/otherVersions',
'Other Versions'
);
const installLabel = nls.localize('arduino/component/install', 'Install');
const installLatestLabel = nls.localize(
'arduino/component/installLatest',
'Install Latest'
);
function installVersionLabel(selectedVersion: string) {
return nls.localize(
'arduino/component/installVersion',
'Install {0}',
selectedVersion
);
}
const updateLabel = nls.localize('arduino/component/update', 'Update');
const removeLabel = nls.localize('arduino/component/remove', 'Remove');
const byLabel = nls.localize('arduino/component/by', 'by');
function nameAuthorLabel(name: string, author: string) {
return nls.localize('arduino/component/title', '{0} by {1}', name, author);
}
function installedLabel(installedVersion: string) {
return nls.localize(
'arduino/component/installed',
'{0} installed',
installedVersion
);
}
function clickToOpenInBrowserLabel(href: string): string | undefined {
return nls.localize(
'arduino/component/clickToOpen',
'Click to open in browser: {0}',
href
);
}
interface MenuTemplate {
readonly menuLabel: string;
}
interface MenuActionTemplate extends MenuTemplate {
readonly menuPath: MenuPath;
readonly handler: CommandHandler;
/**
* If not defined the insertion oder will be the order string.
*/
readonly order?: string;
}
interface SubmenuTemplate extends MenuTemplate {
readonly menuLabel: string;
readonly submenuPath: MenuPath;
readonly options?: SubMenuOptions;
}
function isMenuTemplate(arg: unknown): arg is MenuTemplate {
return (
typeof arg === 'object' &&
(arg as MenuTemplate).menuLabel !== undefined &&
typeof (arg as MenuTemplate).menuLabel === 'string'
);
}
function isMenuActionTemplate(arg: MenuTemplate): arg is MenuActionTemplate {
return (
isMenuTemplate(arg) &&
(arg as MenuActionTemplate).handler !== undefined &&
typeof (arg as MenuActionTemplate).handler === 'object' &&
(arg as MenuActionTemplate).menuPath !== undefined &&
Array.isArray((arg as MenuActionTemplate).menuPath)
);
}
@injectable()
export class ArduinoComponentContextMenuRenderer {
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
private readonly contextMenuRenderer: ContextMenuRenderer;
private readonly toDisposeBeforeRender = new DisposableCollection();
private menuIndexCounter = 0;
async render(
anchor: Anchor,
...templates: (MenuActionTemplate | SubmenuTemplate)[]
): Promise<void> {
this.toDisposeBeforeRender.dispose();
this.toDisposeBeforeRender.pushAll([
Disposable.create(() => (this.menuIndexCounter = 0)),
...templates.map((template) => this.registerMenu(template)),
]);
const options = {
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
anchor,
showDisabled: true,
};
this.contextMenuRenderer.render(options);
}
private registerMenu(
template: MenuActionTemplate | SubmenuTemplate
): Disposable {
if (isMenuActionTemplate(template)) {
const { menuLabel, menuPath, handler, order } = template;
const id = this.generateCommandId(menuLabel, menuPath);
const index = this.menuIndexCounter++;
return new DisposableCollection(
this.commandRegistry.registerCommand({ id }, handler),
this.menuRegistry.registerMenuAction(menuPath, {
commandId: id,
label: menuLabel,
order: typeof order === 'string' ? order : String(index).padStart(4),
})
);
} else {
const { menuLabel, submenuPath, options } = template;
return new DisposableCollection(
this.menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
Disposable.create(() =>
unregisterSubmenu(submenuPath, this.menuRegistry)
)
);
}
}
private generateCommandId(menuLabel: string, menuPath: MenuPath): string {
return `arduino--component-context-${menuPath.join('-')}-${menuLabel}`;
}
}
interface ListItemRendererParams<T extends ArduinoComponent> {
readonly item: T;
readonly selectedVersion: Installable.Version | undefined;
readonly inProgress?: 'installing' | 'uninstalling' | undefined;
readonly install: (item: T) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly onVersionChange: (version: Installable.Version) => void;
}
interface ListItemRendererServices {
readonly windowService: WindowService;
readonly messagesService: MessageService;
readonly commandService: CommandService;
readonly coreService: CoreService;
readonly examplesService: ExamplesService;
readonly sketchesService: SketchesService;
readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer;
}
@injectable()
export class ListItemRenderer<T extends ArduinoComponent> {
@inject(WindowService)
protected windowService: WindowService;
private readonly windowService: WindowService;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(CommandService)
private readonly commandService: CommandService;
@inject(CoreService)
private readonly coreService: CoreService;
@inject(ExamplesService)
private readonly examplesService: ExamplesService;
@inject(SketchesService)
private readonly sketchesService: SketchesService;
@inject(ArduinoComponentContextMenuRenderer)
private readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer;
protected onMoreInfoClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
): void => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
event.nativeEvent.preventDefault();
private readonly onMoreInfo = (href: string | undefined): void => {
if (href) {
this.windowService.openNewWindow(href, { external: true });
}
};
renderItem(
input: ComponentListItem.State & { item: T },
install: (item: T) => Promise<void>,
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
const { item } = input;
let nameAndAuthor: JSX.Element;
if (item.name && item.author) {
const name = <span className="name">{item.name}</span>;
const author = <span className="author">{item.author}</span>;
nameAndAuthor = (
<span>
{name} {nls.localize('arduino/component/by', 'by')} {author}
</span>
);
} else if (item.name) {
nameAndAuthor = <span className="name">{item.name}</span>;
} else if ((item as any).id) {
nameAndAuthor = <span className="name">{(item as any).id}</span>;
} else {
nameAndAuthor = <span className="name">{Unknown}</span>;
}
const onClickUninstall = () => uninstall(item);
const installedVersion = !!item.installedVersion && (
<div className="version-info">
<span className="version">
{nls.localize(
'arduino/component/version',
'Version {0}',
item.installedVersion
)}
</span>
<span
className="installed uppercase"
onClick={onClickUninstall}
{...{
install: nls.localize('arduino/component/installed', 'Installed'),
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
}}
/>
</div>
);
const summary = <div className="summary">{item.summary}</div>;
const description = <div className="summary">{item.description}</div>;
const moreInfo = !!item.moreInfoLink && (
<a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>
{nls.localize('arduino/component/moreInfo', 'More info')}
</a>
);
const onClickInstall = () => install(item);
const installButton = item.installable && (
<button
className="theia-button secondary install uppercase"
onClick={onClickInstall}
>
{nls.localize('arduino/component/install', 'Install')}
</button>
);
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const version = event.target.value;
if (version) {
onVersionChange(version);
}
};
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>;
} else {
return (
<select
className="theia-select"
value={input.selectedVersion}
onChange={onSelectChange}
>
{item.availableVersions
.filter((version) => version !== item.installedVersion) // Filter the version that is currently installed.
.map((version) => (
<option value={version} key={version}>
{version}
</option>
))}
</select>
);
}
})();
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
const action = this.action(params);
return (
<div className="component-list-item noselect">
<div className="header">
{nameAndAuthor}
{installedVersion}
</div>
<div className="content">
{summary}
{description}
</div>
<div className="info">{moreInfo}</div>
<div className="footer">
{versions}
{installButton}
<>
<Separator />
<div className="component-list-item noselect">
<Header
params={params}
action={action}
services={this.services}
onMoreInfo={this.onMoreInfo}
/>
<Content params={params} onMoreInfo={this.onMoreInfo} />
<Footer params={params} action={action} />
</div>
</>
);
}
private action(params: ListItemRendererParams<T>): Installable.Action {
const {
item: { installedVersion, availableVersions },
selectedVersion,
} = params;
return Installable.action({
installed: installedVersion,
available: availableVersions,
selected: selectedVersion,
});
}
private get services(): ListItemRendererServices {
return {
windowService: this.windowService,
messagesService: this.messageService,
commandService: this.commandService,
coreService: this.coreService,
sketchesService: this.sketchesService,
examplesService: this.examplesService,
contextMenuRenderer: this.contextMenuRenderer,
};
}
}
class Separator extends React.Component {
override render(): React.ReactNode {
return (
<div className="separator">
<div />
<div className="line" />
<div />
</div>
);
}
}
class Header<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
action: Installable.Action;
services: ListItemRendererServices;
onMoreInfo: (href: string | undefined) => void;
}>
> {
override render(): React.ReactNode {
return (
<div className="header">
<div>
<Title {...this.props} />
<Toolbar {...this.props} />
</div>
<InstalledVersion {...this.props} />
</div>
);
}
}
class Toolbar<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
action: Installable.Action;
services: ListItemRendererServices;
onMoreInfo: (href: string | undefined) => void;
}>
> {
private readonly onClick = (event: React.MouseEvent): void => {
event.stopPropagation();
event.preventDefault();
const anchor = this.toAnchor(event);
this.showContextMenu(anchor);
};
override render(): React.ReactNode {
return (
<div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}>
<div className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}>
<div
id="__more__"
className={codicon('ellipsis', true)}
title={nls.localizeByDefault('More Actions...')}
onClick={this.onClick}
/>
</div>
</div>
);
}
private toAnchor(event: React.MouseEvent): Anchor {
const itemBox = event.currentTarget
.closest('.' + TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM)
?.getBoundingClientRect();
return itemBox
? {
y: itemBox.bottom + itemBox.height / 2,
x: itemBox.left,
}
: event.nativeEvent;
}
private async showContextMenu(anchor: Anchor): Promise<void> {
this.props.services.contextMenuRenderer.render(
anchor,
this.moreInfo,
...(await this.examples),
...this.otherVersions,
...this.actions
);
}
private get moreInfo(): MenuActionTemplate {
const {
params: {
item: { moreInfoLink },
},
} = this.props;
return {
menuLabel: moreInfoLabel,
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
handler: {
execute: () => this.props.onMoreInfo(moreInfoLink),
isEnabled: () => Boolean(moreInfoLink),
},
};
}
private get examples(): Promise<(MenuActionTemplate | SubmenuTemplate)[]> {
const {
params: {
item,
item: { installedVersion, name },
},
services: { examplesService },
} = this.props;
// TODO: `LibraryPackage.is` should not be here but it saves one extra `lib list`
// gRPC equivalent call with the name of a platform which will result an empty array.
if (!LibraryPackage.is(item) || !installedVersion) {
return Promise.resolve([]);
}
const submenuPath = [
...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
'examples',
];
return examplesService.find({ libraryName: name }).then((containers) => [
{
submenuPath,
menuLabel: examplesLabel,
options: { order: String(0) },
},
...containers
.map((container) => this.flattenContainers(container, submenuPath))
.reduce((acc, curr) => acc.concat(curr), []),
]);
}
private flattenContainers(
container: SketchContainer,
menuPath: MenuPath,
depth = 0
): (MenuActionTemplate | SubmenuTemplate)[] {
const templates: (MenuActionTemplate | SubmenuTemplate)[] = [];
const { label } = container;
if (depth > 0) {
menuPath = [...menuPath, label];
templates.push({
submenuPath: menuPath,
menuLabel: label,
options: { order: label.toLocaleLowerCase() },
});
}
return templates
.concat(
...container.sketches.map((sketch) =>
this.sketchToMenuTemplate(sketch, menuPath)
)
)
.concat(
container.children
.map((childContainer) =>
this.flattenContainers(childContainer, menuPath, ++depth)
)
.reduce((acc, curr) => acc.concat(curr), [])
);
}
private sketchToMenuTemplate(
sketch: SketchRef,
menuPath: MenuPath
): MenuActionTemplate {
const { name, uri } = sketch;
const { sketchesService, commandService } = this.props.services;
return {
menuLabel: name,
menuPath,
handler: {
execute: () =>
openClonedExample(
uri,
{ sketchesService, commandService },
this.onExampleOpenError
),
},
order: name.toLocaleLowerCase(),
};
}
private get onExampleOpenError(): {
onDidFailClone: (
err: ApplicationError<number, unknown>,
uri: string
) => unknown;
onDidFailOpen: (
err: ApplicationError<number, unknown>,
sketch: Sketch
) => unknown;
} {
const {
services: { messagesService, coreService },
} = this.props;
const handle = async (err: ApplicationError<number, unknown>) => {
messagesService.error(err.message);
return coreService.refresh();
};
return {
onDidFailClone: handle,
onDidFailOpen: handle,
};
}
private get otherVersions(): (MenuActionTemplate | SubmenuTemplate)[] {
const {
params: {
item: { availableVersions },
selectedVersion,
onVersionChange,
},
} = this.props;
const submenuPath = [
...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
'other-versions',
];
return [
{
submenuPath,
menuLabel: otherVersionsLabel,
options: { order: String(1) },
},
...availableVersions
.filter((version) => version !== selectedVersion)
.map((version) => ({
menuPath: submenuPath,
menuLabel: version,
handler: {
execute: () => onVersionChange(version),
},
})),
];
}
private get actions(): MenuActionTemplate[] {
const {
action,
params: {
item,
item: { availableVersions, installedVersion },
install,
uninstall,
selectedVersion,
},
} = this.props;
const removeAction = {
menuLabel: removeLabel,
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
handler: {
execute: () => uninstall(item),
},
};
const installAction = {
menuLabel: installVersionLabel(
selectedVersion ?? Installable.latest(availableVersions) ?? ''
),
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
handler: {
execute: () => install(item),
},
};
const installLatestAction = {
menuLabel: installLatestLabel,
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
handler: {
execute: () => install(item),
},
};
const updateAction = {
menuLabel: updateLabel,
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
handler: {
execute: () => install(item),
},
};
switch (action) {
case 'unknown':
return [];
case 'remove': {
return [removeAction];
}
case 'update': {
return [removeAction, updateAction];
}
case 'installLatest':
return [
...(Boolean(installedVersion) ? [removeAction] : []),
installLatestAction,
];
case 'installSelected': {
return [
...(Boolean(installedVersion) ? [removeAction] : []),
installAction,
];
}
}
}
}
class Title<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
}>
> {
override render(): React.ReactNode {
const { name, author } = this.props.params.item;
const title =
name && author ? nameAuthorLabel(name, author) : name ? name : Unknown;
return (
<div className="title" title={title}>
{name && author ? (
<>
{<span className="name">{name}</span>}{' '}
{<span className="author">{`${byLabel} ${author}`}</span>}
</>
) : name ? (
<span className="name">{name}</span>
) : (
<span className="name">{Unknown}</span>
)}
</div>
);
}
}
class InstalledVersion<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
}>
> {
private readonly onClick = (): void => {
this.props.params.uninstall(this.props.params.item);
};
override render(): React.ReactNode {
const { installedVersion } = this.props.params.item;
return (
installedVersion && (
<div className="version">
<span
className="installed-version"
onClick={this.onClick}
{...{
version: installedLabel(installedVersion),
remove: removeLabel,
}}
/>
</div>
)
);
}
}
class Content<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
onMoreInfo: (href: string | undefined) => void;
}>
> {
override render(): React.ReactNode {
const {
params: {
item: { summary, description },
},
} = this.props;
const content = [summary, description].filter(Boolean).join(' ');
return (
<div className="content" title={content}>
<p>{content}</p>
<MoreInfo {...this.props} />
</div>
);
}
}
class MoreInfo<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
onMoreInfo: (href: string | undefined) => void;
}>
> {
private readonly onClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
): void => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.props.onMoreInfo(target.href);
event.nativeEvent.preventDefault();
}
};
override render(): React.ReactNode {
const {
params: {
item: { moreInfoLink: href },
},
} = this.props;
return (
href && (
<div className="info" title={clickToOpenInBrowserLabel(href)}>
<a href={href} onClick={this.onClick}>
{moreInfoLabel}
</a>
</div>
)
);
}
}
class Footer<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
action: Installable.Action;
}>
> {
override render(): React.ReactNode {
return (
<div className="footer">
<SelectVersion {...this.props} />
<Button {...this.props} />
</div>
);
}
}
class SelectVersion<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
action: Installable.Action;
}>
> {
private readonly onChange = (
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const version = event.target.value;
if (version) {
this.props.params.onVersionChange(version);
}
};
override render(): React.ReactNode {
const {
selectedVersion,
item: { availableVersions },
} = this.props.params;
switch (this.props.action) {
case 'installLatest': // fall-through
case 'installSelected': // fall-through
case 'update': // fall-through
case 'remove':
return (
<select
className="theia-select"
value={selectedVersion}
onChange={this.onChange}
>
{availableVersions.map((version) => (
<option value={version} key={version}>
{version}
</option>
))}
</select>
);
case 'unknown':
return undefined;
}
}
}
class Button<T extends ArduinoComponent> extends React.Component<
Readonly<{
params: ListItemRendererParams<T>;
action: Installable.Action;
}>
> {
override render(): React.ReactNode {
const {
params: { item, install, uninstall, inProgress: state },
} = this.props;
const classNames = ['theia-button install uppercase'];
let onClick;
let label;
switch (this.props.action) {
case 'unknown':
return undefined;
case 'installLatest': {
classNames.push('primary');
label = installLabel;
onClick = () => install(item);
break;
}
case 'installSelected': {
classNames.push('secondary');
label = installLabel;
onClick = () => install(item);
break;
}
case 'update': {
classNames.push('secondary');
label = updateLabel;
onClick = () => install(item);
break;
}
case 'remove': {
classNames.push('secondary', 'no-border');
label = removeLabel;
onClick = () => uninstall(item);
break;
}
}
return (
<button
className={classNames.join(' ')}
onClick={onClick}
disabled={Boolean(state)}
>
{label}
</button>
);
}
}

View File

@@ -29,29 +29,27 @@ export abstract class ListWidget<
> extends ReactWidget {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(CommandService)
private readonly commandService: CommandService;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
private focusNode: HTMLElement | undefined;
private readonly didReceiveFirstFocus = new Deferred();
protected readonly searchOptionsChangeEmitter = new Emitter<
private readonly searchOptionsChangeEmitter = new Emitter<
Partial<S> | undefined
>();
private readonly onDidShowEmitter = new Emitter<void>();
/**
* Instead of running an `update` from the `postConstruct` `init` method,
* we use this variable to track first activate, then run.
*/
protected firstActivate = true;
private firstUpdate = true;
constructor(protected options: ListWidget.Options<T, S>) {
super();
@@ -64,7 +62,10 @@ export abstract class ListWidget<
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = undefined;
this.toDispose.push(this.searchOptionsChangeEmitter);
this.toDispose.pushAll([
this.searchOptionsChangeEmitter,
this.onDidShowEmitter,
]);
}
@postConstruct()
@@ -81,12 +82,14 @@ export abstract class ListWidget<
protected override onAfterShow(message: Message): void {
this.maybeUpdateOnFirstRender();
super.onAfterShow(message);
this.onDidShowEmitter.fire();
}
private maybeUpdateOnFirstRender() {
if (this.firstActivate) {
this.firstActivate = false;
if (this.firstUpdate) {
this.firstUpdate = false;
this.update();
this.didReceiveFirstFocus.promise.then(() => this.focusNode?.focus());
}
}
@@ -106,7 +109,9 @@ export abstract class ListWidget<
this.updateScrollBar();
}
protected onFocusResolved = (element: HTMLElement | undefined): void => {
private readonly onFocusResolved = (
element: HTMLElement | undefined
): void => {
this.focusNode = element;
this.didReceiveFirstFocus.resolve();
};
@@ -133,7 +138,7 @@ export abstract class ListWidget<
return this.options.installable.uninstall({ item, progressId });
}
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<FilterableListContainer<T, S>
defaultSearchOptions={this.options.defaultSearchOptions}
@@ -149,6 +154,7 @@ export abstract class ListWidget<
messageService={this.messageService}
commandService={this.commandService}
responseService={this.responseService}
onDidShow={this.onDidShowEmitter.event}
/>
);
}
@@ -186,3 +192,10 @@ export namespace ListWidget {
readonly defaultSearchOptions: S;
}
}
export class UserAbortError extends Error {
constructor(message = 'User abort') {
super(message);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}

View File

@@ -27,17 +27,14 @@ export namespace SketchbookCommands {
export const OPEN_SKETCHBOOK_CONTEXT_MENU: Command = {
id: 'arduino-sketchbook--open-sketch-context-menu',
label: 'Contextual menu',
iconClass: 'sketchbook-tree__opts',
};
export const SKETCHBOOK_HIDE_FILES: Command = {
id: 'arduino-sketchbook--hide-files',
label: 'Contextual menu',
};
export const SKETCHBOOK_SHOW_FILES: Command = {
id: 'arduino-sketchbook--show-files',
label: 'Contextual menu',
};
}

View File

@@ -9,6 +9,7 @@ import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { CommandService } from '@theia/core/lib/common/command';
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
import { CreateNew } from '../sketchbook/create-new';
import { findChildTheiaButton } from '../../utils/dom';
@injectable()
export abstract class BaseSketchbookCompositeWidget<
@@ -18,16 +19,17 @@ export abstract class BaseSketchbookCompositeWidget<
protected readonly commandService: CommandService;
private readonly compositeNode: HTMLElement;
private readonly footerNode: HTMLElement;
private readonly footerRoot: Root;
constructor() {
super();
this.compositeNode = document.createElement('div');
this.compositeNode.classList.add('composite-node');
const footerNode = document.createElement('div');
footerNode.classList.add('footer-node');
this.compositeNode.appendChild(footerNode);
this.footerRoot = createRoot(footerNode);
this.footerNode = document.createElement('div');
this.footerNode.classList.add('footer-node');
this.compositeNode.appendChild(this.footerNode);
this.footerRoot = createRoot(this.footerNode);
this.node.appendChild(this.compositeNode);
this.title.closable = false;
}
@@ -51,6 +53,7 @@ export abstract class BaseSketchbookCompositeWidget<
super.onActivateRequest(message);
// Sending a resize message is needed because otherwise the tree widget would render empty
this.onResize(Widget.ResizeMessage.UnknownSize);
findChildTheiaButton(this.footerNode, true)?.focus();
}
protected override onResize(message: Widget.ResizeMessage): void {

View File

@@ -5,7 +5,7 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import {
NodeProps,
TreeProps,
@@ -23,7 +23,6 @@ import {
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
import { Sketch } from '../../contributions/contribution';
import { nls } from '@theia/core/lib/common';
const customTreeProps: TreeProps = {
@@ -91,8 +90,8 @@ export class SketchbookTreeWidget extends FileTreeWidget {
node: TreeNode,
props: NodeProps
): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) {
return <div className="sketch-folder-icon file-icon"></div>;
if (SketchbookTree.SketchDirNode.is(node)) {
return undefined;
}
const icon = this.toNodeIcon(node);
if (icon) {
@@ -116,7 +115,6 @@ export class SketchbookTreeWidget extends FileTreeWidget {
protected hoveredNodeId: string | undefined;
protected setHoverNodeId(id: string | undefined): void {
this.hoveredNodeId = id;
this.update();
}
protected override createNodeAttributes(
@@ -134,26 +132,34 @@ export class SketchbookTreeWidget extends FileTreeWidget {
protected renderInlineCommands(node: TreeNode): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) && node.commands) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node)
this.renderInlineCommand(command, node)
);
}
return undefined;
}
protected renderInlineCommand(
commandId: string,
command: Command | string | [command: string, label: string],
node: SketchbookTree.SketchDirNode,
options?: any
): React.ReactNode {
const command = this.commandRegistry.getCommand(commandId);
const icon = command?.iconClass;
const commandId = Command.is(command)
? command.id
: Array.isArray(command)
? command[0]
: command;
const resolvedCommand = this.commandRegistry.getCommand(commandId);
const icon = resolvedCommand?.iconClass;
const args = { model: this.model, node: node, ...options };
if (
command &&
resolvedCommand &&
icon &&
this.commandRegistry.isEnabled(commandId, args) &&
this.commandRegistry.isVisible(commandId, args)
) {
const label = Array.isArray(command)
? command[1]
: resolvedCommand.label ?? resolvedCommand.id;
const className = [
TREE_NODE_SEGMENT_CLASS,
TREE_NODE_TAIL_CLASS,
@@ -165,7 +171,7 @@ export class SketchbookTreeWidget extends FileTreeWidget {
<div
key={`${commandId}--${node.id}`}
className={className}
title={command?.label || command.id}
title={label}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();

View File

@@ -9,6 +9,7 @@ import {
WorkspaceRootNode,
} from '@theia/navigator/lib/browser/navigator-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class SketchbookTree extends FileNavigatorTree {
@@ -18,7 +19,9 @@ export class SketchbookTree extends FileNavigatorTree {
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
override async resolveChildren(
parent: CompositeTreeNode
): Promise<TreeNode[]> {
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
@@ -71,7 +74,13 @@ export class SketchbookTree extends FileNavigatorTree {
protected async augmentSketchNode(node: DirNode): Promise<void> {
Object.assign(node, {
type: 'sketch',
commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU],
commands: [
[
'arduino-create-cloud-copy',
nls.localize('arduino/createCloudCopy', 'Push Sketch to Cloud'),
],
SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU,
],
});
}
@@ -96,7 +105,10 @@ export class SketchbookTree extends FileNavigatorTree {
export namespace SketchbookTree {
export interface SketchDirNode extends DirNode {
readonly type: 'sketch';
readonly commands?: Command[];
/**
* Theia command, the command ID string, or a tuple of command ID and preferred UI label. If the array construct is used, the label is the 1<sup>st</sup> of the array.
*/
readonly commands?: (Command | string | [string, string])[];
}
export namespace SketchDirNode {
export function is(

View File

@@ -106,7 +106,7 @@ export class SketchbookWidgetContribution
this.revealSketchNode(treeWidgetId, nodeUri),
});
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
execute: (arg) => this.openNewWindow(arg.node),
execute: (arg) => this.openNewWindow(arg.node, arg?.treeWidgetId),
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) =>
@@ -209,14 +209,20 @@ export class SketchbookWidgetContribution
});
}
private openNewWindow(node: SketchbookTree.SketchDirNode): void {
const widget = this.tryGetWidget();
if (widget) {
const treeWidgetId = widget.activeTreeWidgetId();
if (!treeWidgetId) {
private openNewWindow(
node: SketchbookTree.SketchDirNode,
treeWidgetId?: string
): void {
if (!treeWidgetId) {
const widget = this.tryGetWidget();
if (!widget) {
console.warn(`Could not retrieve active sketchbook tree ID.`);
return;
}
treeWidgetId = widget.activeTreeWidgetId();
}
const widget = this.tryGetWidget();
if (widget) {
const nodeUri = node.uri.toString();
const options: WorkspaceInput = {};
Object.assign(options, {

View File

@@ -128,13 +128,7 @@ export class SketchbookWidget extends BaseWidget {
protected override onActivateRequest(message: Message): void {
super.onActivateRequest(message);
// TODO: focus the active sketchbook
// if (this.editor) {
// this.editor.focus();
// } else {
// }
this.node.focus();
this.sketchbookCompositeWidget.activate();
}
protected override onResize(message: Widget.ResizeMessage): void {

View File

@@ -1,34 +1,35 @@
import { Installable } from './installable';
import type { Installable } from './installable';
export interface ArduinoComponent {
readonly name: string;
readonly deprecated?: boolean;
readonly author: string;
readonly summary: string;
readonly description: string;
readonly moreInfoLink?: string;
readonly availableVersions: Installable.Version[];
readonly installable: boolean;
readonly installedVersion?: Installable.Version;
/**
* This is the `Type` in IDE (1.x) UI.
*/
readonly types: string[];
readonly deprecated?: boolean;
readonly moreInfoLink?: string;
}
export namespace ArduinoComponent {
export function is(arg: any): arg is ArduinoComponent {
export function is(arg: unknown): arg is ArduinoComponent {
return (
!!arg &&
'name' in arg &&
typeof arg['name'] === 'string' &&
'author' in arg &&
typeof arg['author'] === 'string' &&
'summary' in arg &&
typeof arg['summary'] === 'string' &&
'description' in arg &&
typeof arg['description'] === 'string' &&
'installable' in arg &&
typeof arg['installable'] === 'boolean'
typeof arg === 'object' &&
(<ArduinoComponent>arg).name !== undefined &&
typeof (<ArduinoComponent>arg).name === 'string' &&
(<ArduinoComponent>arg).author !== undefined &&
typeof (<ArduinoComponent>arg).author === 'string' &&
(<ArduinoComponent>arg).summary !== undefined &&
typeof (<ArduinoComponent>arg).summary === 'string' &&
(<ArduinoComponent>arg).description !== undefined &&
typeof (<ArduinoComponent>arg).description === 'string' &&
(<ArduinoComponent>arg).availableVersions !== undefined &&
Array.isArray((<ArduinoComponent>arg).availableVersions) &&
(<ArduinoComponent>arg).types !== undefined &&
Array.isArray((<ArduinoComponent>arg).types)
);
}
}

View File

@@ -73,12 +73,12 @@ export namespace CoreError {
UploadUsingProgrammer: 4003,
BurnBootloader: 4004,
};
export const VerifyFailed = create(Codes.Verify);
export const UploadFailed = create(Codes.Upload);
export const UploadUsingProgrammerFailed = create(
export const VerifyFailed = declareCoreError(Codes.Verify);
export const UploadFailed = declareCoreError(Codes.Upload);
export const UploadUsingProgrammerFailed = declareCoreError(
Codes.UploadUsingProgrammer
);
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader);
export function is(
error: unknown
): error is ApplicationError<number, ErrorLocation[]> {
@@ -88,7 +88,7 @@ export namespace CoreError {
Object.values(Codes).includes(error.code)
);
}
function create(
function declareCoreError(
code: number
): ApplicationError.Constructor<number, ErrorLocation[]> {
return ApplicationError.declare(

View File

@@ -9,4 +9,8 @@ export interface ExamplesService {
current: SketchContainer[];
any: SketchContainer[];
}>;
/**
* Finds example sketch containers for the installed library.
*/
find(options: { libraryName: string }): Promise<SketchContainer[]>;
}

View File

@@ -51,6 +51,46 @@ export namespace Installable {
};
}
export const ActionLiterals = [
'installLatest',
'installSelected',
'update',
'remove',
'unknown',
] as const;
export type Action = typeof ActionLiterals[number];
export function action(params: {
installed?: Version | undefined;
available: Version[];
selected?: Version;
}): Action {
const { installed, available } = params;
const latest = Installable.latest(available);
if (!latest || (installed && !available.includes(installed))) {
return 'unknown';
}
const selected = params.selected ?? latest;
if (installed === selected) {
return 'remove';
}
if (installed) {
return selected === latest && installed !== latest
? 'update'
: 'installSelected';
} else {
return selected === latest ? 'installLatest' : 'installSelected';
}
}
export function latest(versions: Version[]): Version | undefined {
if (!versions.length) {
return undefined;
}
const ordered = versions.slice().sort(Installable.Version.COMPARATOR);
return ordered[ordered.length - 1];
}
export const Installed = <T extends ArduinoComponent>({
installedVersion,
}: T): boolean => {

View File

@@ -198,6 +198,10 @@ export namespace LibraryService {
export namespace List {
export interface Options {
readonly fqbn?: string | undefined;
/**
* The name of the library to filter to.
*/
readonly libraryName?: string | undefined;
}
}
}
@@ -241,11 +245,15 @@ export interface LibraryPackage extends ArduinoComponent {
readonly category: string;
}
export namespace LibraryPackage {
export function is(arg: any): arg is LibraryPackage {
export function is(arg: unknown): arg is LibraryPackage {
return (
ArduinoComponent.is(arg) &&
'includes' in arg &&
Array.isArray(arg['includes'])
(<LibraryPackage>arg).includes !== undefined &&
Array.isArray((<LibraryPackage>arg).includes) &&
(<LibraryPackage>arg).exampleUris !== undefined &&
Array.isArray((<LibraryPackage>arg).exampleUris) &&
(<LibraryPackage>arg).location !== undefined &&
typeof (<LibraryPackage>arg).location === 'number'
);
}

View File

@@ -1,4 +1,4 @@
import { Event, JsonRpcServer } from '@theia/core';
import { ApplicationError, Event, JsonRpcServer, nls } from '@theia/core';
import {
PluggableMonitorSettings,
MonitorSettings,
@@ -31,7 +31,7 @@ export interface MonitorManagerProxyClient {
onMessagesReceived: Event<{ messages: string[] }>;
onMonitorSettingsDidChange: Event<MonitorSettings>;
onMonitorShouldReset: Event<void>;
connect(addressPort: number): void;
connect(addressPort: number): Promise<void>;
disconnect(): void;
getWebSocketPort(): number | undefined;
isWSConnected(): Promise<boolean>;
@@ -46,7 +46,7 @@ export interface PluggableMonitorSetting {
readonly id: string;
// A human-readable label of the setting (to be displayed on the GUI)
readonly label: string;
// The setting type (at the moment only "enum" is avaiable)
// The setting type (at the moment only "enum" is available)
readonly type: string;
// The values allowed on "enum" types
readonly values: string[];
@@ -72,24 +72,168 @@ export namespace Monitor {
};
}
export interface Status {}
export type OK = Status;
export interface ErrorStatus extends Status {
readonly message: string;
}
export namespace Status {
export function isOK(status: Status & { message?: string }): status is OK {
return !!status && typeof status.message !== 'string';
export const MonitorErrorCodes = {
ConnectionFailed: 6001,
NotConnected: 6002,
AlreadyConnected: 6003,
MissingConfiguration: 6004,
} as const;
export const ConnectionFailedError = declareMonitorError(
MonitorErrorCodes.ConnectionFailed
);
export const NotConnectedError = declareMonitorError(
MonitorErrorCodes.NotConnected
);
export const AlreadyConnectedError = declareMonitorError(
MonitorErrorCodes.AlreadyConnected
);
export const MissingConfigurationError = declareMonitorError(
MonitorErrorCodes.MissingConfiguration
);
export function createConnectionFailedError(
port: Port,
details?: string
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
let message;
if (details) {
const detailsWithPeriod = details.endsWith('.') ? details : `${details}.`;
message = nls.localize(
'arduino/monitor/connectionFailedErrorWithDetails',
'{0} Could not connect to {1} {2} port.',
detailsWithPeriod,
address,
protocol
);
} else {
message = nls.localize(
'arduino/monitor/connectionFailedError',
'Could not connect to {0} {1} port.',
address,
protocol
);
}
export const OK: OK = {};
export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
export const ALREADY_CONNECTED: ErrorStatus = {
message: 'Already connected.',
};
export const CONFIG_MISSING: ErrorStatus = {
message: 'Serial Config missing.',
};
export const UPLOAD_IN_PROGRESS: ErrorStatus = {
message: 'Upload in progress.',
};
return ConnectionFailedError(message, { protocol, address });
}
export function createNotConnectedError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return NotConnectedError(
nls.localize(
'arduino/monitor/notConnectedError',
'Not connected to {0} {1} port.',
address,
protocol
),
{ protocol, address }
);
}
export function createAlreadyConnectedError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return AlreadyConnectedError(
nls.localize(
'arduino/monitor/alreadyConnectedError',
'Could not connect to {0} {1} port. Already connected.',
address,
protocol
),
{ protocol, address }
);
}
export function createMissingConfigurationError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return MissingConfigurationError(
nls.localize(
'arduino/monitor/missingConfigurationError',
'Could not connect to {0} {1} port. The monitor configuration is missing.',
address,
protocol
),
{ protocol, address }
);
}
/**
* Bare minimum representation of a port. Supports neither UI labels nor properties.
*/
interface PortDescriptor {
readonly protocol: string;
readonly address: string;
}
function declareMonitorError(
code: number
): ApplicationError.Constructor<number, PortDescriptor> {
return ApplicationError.declare(
code,
(message: string, data: PortDescriptor) => ({ data, message })
);
}
export interface MonitorConnectionError {
readonly errorMessage: string;
}
export type MonitorConnectionStatus =
| 'connecting'
| 'connected'
| 'not-connected'
| MonitorConnectionError;
export function monitorConnectionStatusEquals(
left: MonitorConnectionStatus,
right: MonitorConnectionStatus
): boolean {
if (typeof left === 'object' && typeof right === 'object') {
return left.errorMessage === right.errorMessage;
}
return left === right;
}
/**
* @deprecated see `MonitorState#connected`
*/
export function isMonitorConnected(
status: MonitorConnectionStatus
): status is 'connected' {
return status === 'connected';
}
export function isMonitorConnectionError(
status: MonitorConnectionStatus
): status is MonitorConnectionError {
return typeof status === 'object';
}
export interface MonitorState {
autoscroll: boolean;
timestamp: boolean;
lineEnding: MonitorEOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connectionStatus: MonitorConnectionStatus;
/**
* @deprecated This property is never get by IDE2 only set. This value is present to be backward compatible with the plotter app.
* IDE2 uses `MonitorState#connectionStatus`.
*/
connected: boolean;
}
export namespace MonitorState {
export interface Change<K extends keyof MonitorState> {
readonly property: K;
readonly value: MonitorState[K];
}
}
export type MonitorEOL = '' | '\n' | '\r' | '\r\n';
export namespace MonitorEOL {
export const DEFAULT: MonitorEOL = '\n';
}

View File

@@ -74,12 +74,15 @@ export interface SketchesService {
isTemp(sketch: SketchRef): Promise<boolean>;
/**
* If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp
* location to `directories.user`. Resolves with the URI of the sketch after the move. Rejects, when the sketch
* was not in the temp folder. This method always overrides. It's the callers responsibility to ask the user whether
* the files at the destination can be overwritten or not.
* Recursively copies the sketch folder content including all files into the destination folder.
* Resolves with the new URI of the sketch after the move. This method always overrides. It's the callers responsibility to ask the user whether
* the files at the destination can be overwritten or not. This method copies all filesystem files, if you want to copy only sketch files,
* but exclude, for example, language server log file, set the `onlySketchFiles` property to `true`. `onlySketchFiles` is `false` by default.
*/
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
copy(
sketch: Sketch,
options: { destinationUri: string; onlySketchFiles?: boolean }
): Promise<Sketch>;
/**
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.
@@ -157,8 +160,6 @@ export namespace Sketch {
// (non-API) exported for the tests
export const defaultSketchFolderName = 'sketch';
// (non-API) exported for the tests
export const defaultFallbackFirstChar = '0';
// (non-API) exported for the tests
export const defaultFallbackChar = '_';
// (non-API) exported for the tests
export function reservedFilename(name: string): string {
@@ -176,11 +177,11 @@ export namespace Sketch {
// (non-API) exported for the tests
export const invalidSketchFolderNameMessage = nls.localize(
'arduino/sketch/invalidSketchName',
'The name must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.'
'The name must start with a letter, number, or underscore, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.'
);
const invalidCloudSketchFolderNameMessage = nls.localize(
'arduino/sketch/invalidCloudSketchName',
'The name must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 36 characters.'
'The name must start with a letter, number, or underscore, followed by letters, numbers, dashes, dots and underscores. Maximum length is 36 characters.'
);
/**
* `undefined` if the candidate sketch folder name is valid. Otherwise, the validation error message.
@@ -193,7 +194,7 @@ export namespace Sketch {
if (validFilenameError) {
return validFilenameError;
}
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate)
return /^[0-9a-zA-Z_]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate)
? undefined
: invalidSketchFolderNameMessage;
}
@@ -208,7 +209,7 @@ export namespace Sketch {
if (validFilenameError) {
return validFilenameError;
}
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,35}$/.test(candidate)
return /^[0-9a-zA-Z_]{1}[0-9a-zA-Z_\.-]{0,35}$/.test(candidate)
? undefined
: invalidCloudSketchFolderNameMessage;
}
@@ -252,10 +253,7 @@ export namespace Sketch {
return defaultSketchFolderName;
}
const validName = candidate
? candidate
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)
.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar)
.slice(0, 63)
? candidate.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar).slice(0, 63)
: defaultSketchFolderName;
if (appendTimestampSuffix) {
return `${validName.slice(0, 63 - timestampSuffixLength)}${
@@ -283,10 +281,7 @@ export namespace Sketch {
return defaultSketchFolderName;
}
return candidate
? candidate
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)
.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar)
.slice(0, 36)
? candidate.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar).slice(0, 36)
: defaultSketchFolderName;
}

View File

@@ -20,3 +20,21 @@ export function startsWithUpperCase(what: string): boolean {
export function isNullOrUndefined(what: unknown): what is undefined | null {
return what === undefined || what === null;
}
// Use it for and exhaustive `switch` statements
// https://stackoverflow.com/a/39419171/5529090
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function assertUnreachable(_: never): never {
throw new Error();
}
// Text encoder can crash in electron browser: https://github.com/arduino/arduino-ide/issues/634#issuecomment-1440039171
export function uint8ArrayToString(uint8Array: Uint8Array): string {
return uint8Array.reduce(
(text, byte) => text + String.fromCharCode(byte),
''
);
}
export function stringToUint8Array(text: string): Uint8Array {
return Uint8Array.from(text, (char) => char.charCodeAt(0));
}

View File

@@ -1,5 +1,4 @@
import { join } from 'path';
import { promises as fs } from 'fs';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { spawn, ChildProcess } from 'child_process';
import { FileUri } from '@theia/core/lib/node/file-uri';
@@ -16,7 +15,7 @@ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-app
import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol';
import { CLI_CONFIG } from './cli-config';
import { getExecPath } from './exec-util';
import { ErrnoException } from './utils/errors';
import { SettingsReader } from './settings-reader';
@injectable()
export class ArduinoDaemonImpl
@@ -32,6 +31,9 @@ export class ArduinoDaemonImpl
@inject(NotificationServiceServer)
private readonly notificationService: NotificationServiceServer;
@inject(SettingsReader)
private readonly settingsReader: SettingsReader;
private readonly toDispose = new DisposableCollection();
private readonly onDaemonStartedEmitter = new Emitter<string>();
private readonly onDaemonStoppedEmitter = new Emitter<void>();
@@ -134,8 +136,6 @@ export class ArduinoDaemonImpl
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
const args = [
'daemon',
'--format',
'jsonmini',
'--port',
'0',
'--config-file',
@@ -149,34 +149,12 @@ export class ArduinoDaemonImpl
}
private async debugDaemon(): Promise<boolean> {
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
const configDirUri = await this.envVariablesServer.getConfigDirUri();
const configDirPath = FileUri.fsPath(configDirUri);
try {
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
encoding: 'utf8',
});
const json = this.tryParse(raw);
if (json) {
const value = json['arduino.cli.daemon.debug'];
return typeof value === 'boolean' && !!value;
}
return false;
} catch (error) {
if (ErrnoException.isENOENT(error)) {
return false;
}
throw error;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private tryParse(raw: string): any | undefined {
try {
return JSON.parse(raw);
} catch {
return undefined;
const settings = await this.settingsReader.read();
if (settings) {
const value = settings['arduino.cli.daemon.debug'];
return value === true;
}
return false;
}
protected async spawnDaemonProcess(): Promise<{
@@ -197,26 +175,6 @@ export class ArduinoDaemonImpl
daemon.stdout.on('data', (data) => {
const message = data.toString();
let port = '';
let address = '';
message
.split('\n')
.filter((line: string) => line.length)
.forEach((line: string) => {
try {
const parsedLine = JSON.parse(line);
if ('Port' in parsedLine) {
port = parsedLine.Port;
}
if ('IP' in parsedLine) {
address = parsedLine.IP;
}
} catch (err) {
// ignore
}
});
this.onData(message);
if (!grpcServerIsReady) {
const error = DaemonError.parse(message);
@@ -225,6 +183,25 @@ export class ArduinoDaemonImpl
return;
}
let port = '';
let address = '';
message
.split('\n')
.filter((line: string) => line.length)
.forEach((line: string) => {
try {
const parsedLine = JSON.parse(line);
if ('Port' in parsedLine) {
port = parsedLine.Port;
}
if ('IP' in parsedLine) {
address = parsedLine.IP;
}
} catch (err) {
// ignore
}
});
if (port.length && address.length) {
grpcServerIsReady = true;
ready.resolve({ daemon, port });

View File

@@ -118,6 +118,7 @@ import {
LocalDirectoryPluginDeployerResolverWithFallback,
PluginDeployer_GH_12064,
} from './theia/plugin-ext/plugin-deployer';
import { SettingsReader } from './settings-reader';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@@ -403,6 +404,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.toSelf()
.inSingletonScope();
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
bind(SettingsReader).toSelf().inSingletonScope();
});
function bindChildLogger(bind: interfaces.Bind, name: string): void {

View File

@@ -1333,5 +1333,3 @@ enumerateMonitorPortSettings: {
},
};
// BOOTSTRAP COMMANDS
// -------------------

View File

@@ -172,6 +172,31 @@ export namespace InitResponse {
}
export class FailedInstanceInitError extends jspb.Message {
getReason(): FailedInstanceInitReason;
setReason(value: FailedInstanceInitReason): FailedInstanceInitError;
getMessage(): string;
setMessage(value: string): FailedInstanceInitError;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): FailedInstanceInitError.AsObject;
static toObject(includeInstance: boolean, msg: FailedInstanceInitError): FailedInstanceInitError.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: FailedInstanceInitError, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): FailedInstanceInitError;
static deserializeBinaryFromReader(message: FailedInstanceInitError, reader: jspb.BinaryReader): FailedInstanceInitError;
}
export namespace FailedInstanceInitError {
export type AsObject = {
reason: FailedInstanceInitReason,
message: string,
}
}
export class DestroyRequest extends jspb.Message {
hasInstance(): boolean;
@@ -528,3 +553,10 @@ export namespace ArchiveSketchResponse {
export type AsObject = {
}
}
export enum FailedInstanceInitReason {
FAILED_INSTANCE_INIT_REASON_UNSPECIFIED = 0,
FAILED_INSTANCE_INIT_REASON_INVALID_INDEX_URL = 1,
FAILED_INSTANCE_INIT_REASON_INDEX_LOAD_ERROR = 2,
FAILED_INSTANCE_INIT_REASON_TOOL_LOAD_ERROR = 3,
}

View File

@@ -37,6 +37,8 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.CreateRequest', null, global
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.CreateResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DestroyRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DestroyResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.FailedInstanceInitError', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InitRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InitResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InitResponse.MessageCase', null, global);
@@ -156,6 +158,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.cc.arduino.cli.commands.v1.InitResponse.Progress.displayName = 'proto.cc.arduino.cli.commands.v1.InitResponse.Progress';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.FailedInstanceInitError, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.displayName = 'proto.cc.arduino.cli.commands.v1.FailedInstanceInitError';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -1398,6 +1421,166 @@ proto.cc.arduino.cli.commands.v1.InitResponse.prototype.hasProfile = function()
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.toObject = function(includeInstance, msg) {
var f, obj = {
reason: jspb.Message.getFieldWithDefault(msg, 1, 0),
message: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.FailedInstanceInitError;
return proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason} */ (reader.readEnum());
msg.setReason(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setMessage(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getReason();
if (f !== 0.0) {
writer.writeEnum(
1,
f
);
}
f = message.getMessage();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
};
/**
* optional FailedInstanceInitReason reason = 1;
* @return {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.getReason = function() {
return /** @type {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
};
/**
* @param {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason} value
* @return {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError} returns this
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.setReason = function(value) {
return jspb.Message.setProto3EnumField(this, 1, value);
};
/**
* optional string message = 2;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.getMessage = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.FailedInstanceInitError} returns this
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitError.prototype.setMessage = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
@@ -3699,4 +3882,14 @@ proto.cc.arduino.cli.commands.v1.ArchiveSketchResponse.serializeBinaryToWriter =
};
/**
* @enum {number}
*/
proto.cc.arduino.cli.commands.v1.FailedInstanceInitReason = {
FAILED_INSTANCE_INIT_REASON_UNSPECIFIED: 0,
FAILED_INSTANCE_INIT_REASON_INVALID_INDEX_URL: 1,
FAILED_INSTANCE_INIT_REASON_INDEX_LOAD_ERROR: 2,
FAILED_INSTANCE_INIT_REASON_TOOL_LOAD_ERROR: 3
};
goog.object.extend(exports, proto.cc.arduino.cli.commands.v1);

View File

@@ -48,9 +48,6 @@ export class CompileRequest extends jspb.Message {
getQuiet(): boolean;
setQuiet(value: boolean): CompileRequest;
getVidPid(): string;
setVidPid(value: string): CompileRequest;
getJobs(): number;
setJobs(value: number): CompileRequest;
@@ -122,7 +119,6 @@ export namespace CompileRequest {
warnings: string,
verbose: boolean,
quiet: boolean,
vidPid: string,
jobs: number,
librariesList: Array<string>,
optimizeForDebug: boolean,

View File

@@ -137,7 +137,6 @@ proto.cc.arduino.cli.commands.v1.CompileRequest.toObject = function(includeInsta
warnings: jspb.Message.getFieldWithDefault(msg, 9, ""),
verbose: jspb.Message.getBooleanFieldWithDefault(msg, 10, false),
quiet: jspb.Message.getBooleanFieldWithDefault(msg, 11, false),
vidPid: jspb.Message.getFieldWithDefault(msg, 12, ""),
jobs: jspb.Message.getFieldWithDefault(msg, 14, 0),
librariesList: (f = jspb.Message.getRepeatedField(msg, 15)) == null ? undefined : f,
optimizeForDebug: jspb.Message.getBooleanFieldWithDefault(msg, 16, false),
@@ -232,10 +231,6 @@ proto.cc.arduino.cli.commands.v1.CompileRequest.deserializeBinaryFromReader = fu
var value = /** @type {boolean} */ (reader.readBool());
msg.setQuiet(value);
break;
case 12:
var value = /** @type {string} */ (reader.readString());
msg.setVidPid(value);
break;
case 14:
var value = /** @type {number} */ (reader.readInt32());
msg.setJobs(value);
@@ -398,13 +393,6 @@ proto.cc.arduino.cli.commands.v1.CompileRequest.serializeBinaryToWriter = functi
f
);
}
f = message.getVidPid();
if (f.length > 0) {
writer.writeString(
12,
f
);
}
f = message.getJobs();
if (f !== 0) {
writer.writeInt32(
@@ -733,24 +721,6 @@ proto.cc.arduino.cli.commands.v1.CompileRequest.prototype.setQuiet = function(va
};
/**
* optional string vid_pid = 12;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.CompileRequest.prototype.getVidPid = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 12, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.CompileRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.CompileRequest.prototype.setVidPid = function(value) {
return jspb.Message.setProto3StringField(this, 12, value);
};
/**
* optional int32 jobs = 14;
* @return {number}

View File

@@ -82,6 +82,23 @@ export namespace PlatformInstallResponse {
}
}
export class PlatformLoadingError extends jspb.Message {
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): PlatformLoadingError.AsObject;
static toObject(includeInstance: boolean, msg: PlatformLoadingError): PlatformLoadingError.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: PlatformLoadingError, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): PlatformLoadingError;
static deserializeBinaryFromReader(message: PlatformLoadingError, reader: jspb.BinaryReader): PlatformLoadingError;
}
export namespace PlatformLoadingError {
export type AsObject = {
}
}
export class PlatformDownloadRequest extends jspb.Message {
hasInstance(): boolean;

View File

@@ -24,6 +24,7 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformInstallRequest', nul
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformInstallResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformListRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformListResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformLoadingError', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformSearchRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformSearchResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.PlatformUninstallRequest', null, global);
@@ -72,6 +73,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.cc.arduino.cli.commands.v1.PlatformInstallResponse.displayName = 'proto.cc.arduino.cli.commands.v1.PlatformInstallResponse';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.PlatformLoadingError, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.displayName = 'proto.cc.arduino.cli.commands.v1.PlatformLoadingError';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -809,6 +831,107 @@ proto.cc.arduino.cli.commands.v1.PlatformInstallResponse.prototype.hasTaskProgre
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.PlatformLoadingError.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.PlatformLoadingError} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.toObject = function(includeInstance, msg) {
var f, obj = {
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.PlatformLoadingError}
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.PlatformLoadingError;
return proto.cc.arduino.cli.commands.v1.PlatformLoadingError.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.PlatformLoadingError} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.PlatformLoadingError}
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.PlatformLoadingError} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.PlatformLoadingError.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.

View File

@@ -406,6 +406,9 @@ export class LibrarySearchRequest extends jspb.Message {
getQuery(): string;
setQuery(value: string): LibrarySearchRequest;
getOmitReleasesDetails(): boolean;
setOmitReleasesDetails(value: boolean): LibrarySearchRequest;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): LibrarySearchRequest.AsObject;
@@ -421,6 +424,7 @@ export namespace LibrarySearchRequest {
export type AsObject = {
instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject,
query: string,
omitReleasesDetails: boolean,
}
}
@@ -465,6 +469,11 @@ export class SearchedLibrary extends jspb.Message {
getLatest(): LibraryRelease | undefined;
setLatest(value?: LibraryRelease): SearchedLibrary;
clearAvailableVersionsList(): void;
getAvailableVersionsList(): Array<string>;
setAvailableVersionsList(value: Array<string>): SearchedLibrary;
addAvailableVersions(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): SearchedLibrary.AsObject;
@@ -482,6 +491,7 @@ export namespace SearchedLibrary {
releasesMap: Array<[string, LibraryRelease.AsObject]>,
latest?: LibraryRelease.AsObject,
availableVersionsList: Array<string>,
}
}

View File

@@ -374,7 +374,7 @@ if (goog.DEBUG && !COMPILED) {
* @constructor
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
jspb.Message.initialize(this, opt_data, 0, -1, proto.cc.arduino.cli.commands.v1.SearchedLibrary.repeatedFields_, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.SearchedLibrary, jspb.Message);
if (goog.DEBUG && !COMPILED) {
@@ -3202,7 +3202,8 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.toObject = funct
proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.toObject = function(includeInstance, msg) {
var f, obj = {
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f),
query: jspb.Message.getFieldWithDefault(msg, 2, "")
query: jspb.Message.getFieldWithDefault(msg, 2, ""),
omitReleasesDetails: jspb.Message.getBooleanFieldWithDefault(msg, 3, false)
};
if (includeInstance) {
@@ -3248,6 +3249,10 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.deserializeBinaryFromReade
var value = /** @type {string} */ (reader.readString());
msg.setQuery(value);
break;
case 3:
var value = /** @type {boolean} */ (reader.readBool());
msg.setOmitReleasesDetails(value);
break;
default:
reader.skipField();
break;
@@ -3292,6 +3297,13 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.serializeBinaryToWriter =
f
);
}
f = message.getOmitReleasesDetails();
if (f) {
writer.writeBool(
3,
f
);
}
};
@@ -3350,6 +3362,24 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.setQuery = funct
};
/**
* optional bool omit_releases_details = 3;
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.getOmitReleasesDetails = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false));
};
/**
* @param {boolean} value
* @return {!proto.cc.arduino.cli.commands.v1.LibrarySearchRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibrarySearchRequest.prototype.setOmitReleasesDetails = function(value) {
return jspb.Message.setProto3BooleanField(this, 3, value);
};
/**
* List of repeated fields within this message type.
@@ -3541,6 +3571,13 @@ proto.cc.arduino.cli.commands.v1.LibrarySearchResponse.prototype.setStatus = fun
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary.repeatedFields_ = [4];
if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -3574,7 +3611,8 @@ proto.cc.arduino.cli.commands.v1.SearchedLibrary.toObject = function(includeInst
var f, obj = {
name: jspb.Message.getFieldWithDefault(msg, 1, ""),
releasesMap: (f = msg.getReleasesMap()) ? f.toObject(includeInstance, proto.cc.arduino.cli.commands.v1.LibraryRelease.toObject) : [],
latest: (f = msg.getLatest()) && proto.cc.arduino.cli.commands.v1.LibraryRelease.toObject(includeInstance, f)
latest: (f = msg.getLatest()) && proto.cc.arduino.cli.commands.v1.LibraryRelease.toObject(includeInstance, f),
availableVersionsList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f
};
if (includeInstance) {
@@ -3626,6 +3664,10 @@ proto.cc.arduino.cli.commands.v1.SearchedLibrary.deserializeBinaryFromReader = f
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.LibraryRelease.deserializeBinaryFromReader);
msg.setLatest(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.addAvailableVersions(value);
break;
default:
reader.skipField();
break;
@@ -3674,6 +3716,13 @@ proto.cc.arduino.cli.commands.v1.SearchedLibrary.serializeBinaryToWriter = funct
proto.cc.arduino.cli.commands.v1.LibraryRelease.serializeBinaryToWriter
);
}
f = message.getAvailableVersionsList();
if (f.length > 0) {
writer.writeRepeatedString(
4,
f
);
}
};
@@ -3754,6 +3803,43 @@ proto.cc.arduino.cli.commands.v1.SearchedLibrary.prototype.hasLatest = function(
};
/**
* repeated string available_versions = 4;
* @return {!Array<string>}
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary.prototype.getAvailableVersionsList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 4));
};
/**
* @param {!Array<string>} value
* @return {!proto.cc.arduino.cli.commands.v1.SearchedLibrary} returns this
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary.prototype.setAvailableVersionsList = function(value) {
return jspb.Message.setField(this, 4, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.cc.arduino.cli.commands.v1.SearchedLibrary} returns this
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary.prototype.addAvailableVersions = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 4, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.cc.arduino.cli.commands.v1.SearchedLibrary} returns this
*/
proto.cc.arduino.cli.commands.v1.SearchedLibrary.prototype.clearAvailableVersionsList = function() {
return this.setAvailableVersionsList([]);
};
/**
* List of repeated fields within this message type.

View File

@@ -23,7 +23,7 @@ import {
UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service';
import { OutputMessage, Port, Status } from '../common/protocol';
import { OutputMessage, Port } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, CommandService, Disposable, nls } from '@theia/core';
@@ -392,7 +392,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}: {
fqbn?: string | undefined;
port?: Port | undefined;
}): Promise<Status> {
}): Promise<void> {
this.boardDiscovery.setUploadInProgress(false);
return this.monitorManager.notifyUploadFinished(fqbn, port);
}

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