Compare commits

...

198 Commits
2.1.1 ... main

Author SHA1 Message Date
Giacomo Cusinato
0f9f0d07b7
chore: switch to version 2.3.7 after the release (#2701) 2025-04-09 19:48:50 +07:00
Giacomo Cusinato
2f0414a5a1
fix: use ElectronConnectionHandler to connect ide updater services (#2697) 2025-04-09 13:58:47 +07:00
github-actions[bot]
e3319dab1a
Updated translation files (#2692)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-08 16:14:03 +07:00
Giacomo Cusinato
a669a43449
fix: wait for theia initalWindow to be set before opening sketch through open-file event (#2693)
* chore: use actual app name as `uriScheme`

* fix: wait for `initialWindow` to be set before opening sketch from file
2025-04-07 19:15:59 +02:00
Giacomo Cusinato
56ab874177
fix: propagate electron params in second instance startup (#2686) 2025-04-05 19:56:00 +09:00
Giacomo Cusinato
e36f393682
fix: prevent OutputWidget to gain focus when updated (#2681) 2025-04-03 17:51:30 +02:00
Giacomo Cusinato
4d52bb2843
chore: switch to version 2.3.6 after the release (#2682) 2025-04-03 17:51:12 +02:00
Giacomo Cusinato
39c8db8e90
fix: add missing linux dependencies for create-changelog job (#2677) 2025-04-02 23:24:35 +09:00
Giacomo Cusinato
8aa3c28c50
fix: allow write permission on release job (#2676)
Permission were previously changed here https://github.com/arduino/arduino-ide/pull/2651
Repo write permission is needed to allow creating the github release and publishing files
2025-04-02 21:51:35 +09:00
Giacomo Cusinato
d293595b89
fix: add missing linux dependencies (#2675) 2025-04-02 09:23:57 +09:00
Giacomo Cusinato
4b0982ccb3
fix: safer electron version parsing for electron-builder command (#2673) 2025-04-01 18:42:04 +09:00
Per Tillisch
9b15695c60
Give build workflow step access to required deployment environment (#2672)
* Trim trailing whitespace in build workflow

* Give build workflow step access to required deployment environment

Certain operations in the "Arduino IDE" GitHub Actions workflow use GitHub Actions "secrets" which are defined in the
repository's administrative settings.

These secrets will typically not be defined when the workflow is run in a fork. However, the workflow's base
functionality, the automated building of the application, does not require secrets. Since that base functionality alone
is very useful to contributors (either to validate relevant changes to the application and infrastructure, or to
generate tester builds) who are performing development work in a fork. For this reason, the workflow is configured to
only perform the secret-dependent operations when the required secrets have been defined in the repository settings.

One such operation is publishing the generated builds to Amazon S3, which Arduino uses to host files for distribution.
This operation depends on the "AWS_ROLE_ARN" secret. As a security measure, this secret is defined inside a deployment
environment (named "production"). GitHub Actions workflow jobs can only use secrets from deployment environments which
they have been explicitly configured to have access to.

At the time the workflow was originally developed, GitHub did not have the deployment environment feature, and so the
workflow was not configured to use environments. The switch to using a deployment environment for this secret was made
only recently, and when that was done, the workflow job that checks whether the secret is defined was not configured to
have access to the "production" environment. This caused the workflow to think it was running in a context where that
secret is not defined even when the secret is in fact defined. The bug caused the workflow to always spuriously skip the
"publish" job which publishes nightly builds of Arduino IDE, and the "publish release" step which publishes production
releases.

The bug is fixed by configuring the "build-type-determination" job so that it has access to the "production"
environment.
2025-04-01 16:27:06 +09:00
github-actions[bot]
0dff87e29c
Updated translation files (#2597)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-31 19:42:48 +09:00
Giacomo Cusinato
7dafe7b0d3
feat: use Arduino CLI 1.2.0 (#2645) 2025-03-28 17:53:46 +01:00
Giacomo Cusinato
859d29d41a
feat: use theia@1.57.0 (#2654) 2025-03-29 01:33:25 +09:00
Christian Sarnataro
d298b3ffc9
fix: sanitize message in notification component (#2664)
fix: sanitize messages in notification component
2025-03-24 12:42:48 +01:00
Giacomo Cusinato
9ab87bf8b5 chore: use AWS OpenID Connect for S3 publish 2025-03-11 21:42:12 +07:00
dankeboy36
5ec1915000
fix(plugin): decouple state update from the LS (#2643)
* fix(plugin): decouple state update from the LS

To enhance the reliability of Arduino IDE extensions, the update
process for `ArduinoState` has been modified to ensure independence
from the language server's availability. This change addresses issues
caused by `compileSummary` being `undefined` due to potential startup
failures of the Arduino Language Server, as noted in
https://github.com/dankeboy36/esp-exception-decoder/issues/28#issuecomment-2681800772.

The `compile` command now resolves with a `CompileSummary` rather than
`void`, facilitating a more reliable way for extensions to access
necessary data. Furthermore, the command has been adjusted to allow
resolution with `undefined` when the compiled data is partial.

By transitioning to direct usage of the resolved compile value for
state updates, the reliance on executed commands for extensions is
eliminated. This update also moves the VSIX command execution to the
frontend without altering existing IDE behavior.

Closes arduino/arduino-ide#2642

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: install missing libx11-dev and libxkbfile-dev

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: pick better GH step name

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: install the required dependencies on Linux

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(revert): do not manually install deps on Linux

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* chore: pin `ubuntu-22.04` for linux actions

* fix: restore accidentally removed dispose on finally

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(test): align mock naming 💄

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: let the ino contribution notify the LS

+ event emitter dispatches the new state.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix(test): emit the new compiler summary state

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* chore(revert): unpin linux version, use latest

revert of b11bde1c47

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
Co-authored-by: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com>
2025-03-10 15:20:22 +07:00
Per Tillisch
6d96e227eb Bump built-in example sketches version to 1.10.2
The Arduino IDE installation includes a collection of example sketches demonstrating fundamental concepts.

These examples are hosted in a dedicated repository, which is a dependency of this project. A new release has been made
in that `arduino/arduino-examples` repository.

The infrastructure for downloading the examples during the Arduino IDE build is hereby updated to use the latest release
of the `arduino/arduino-examples` repository.
2025-02-28 06:56:33 -08:00
dankeboy36
1712f9ea9d fix: install missing linux dependencies
Install `libx11-dev`, `libxkbfile-dev`, `libsecret-1-dev` libraries as the most recent update to ubuntu-latest does not include them
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2025-02-28 19:15:04 +07:00
Giacomo Cusinato
6eef09efd8
chore: switch to version 2.3.5 after the release (#2587)
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.
2024-12-03 13:21:25 +01:00
github-actions[bot]
1112057979
Updated translation files (#2523)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-02 16:02:58 +01:00
Giacomo Cusinato
8e18c47d30 feat: use dompurify to sanitize translations
Pin same version of `dompurify` used in Theia
2024-12-02 15:21:22 +01:00
Giacomo Cusinato
4788bfbc3f feat: introduce VersionWelcomeDialog
Show donate dialog after the first time a first IDE version is loaded
2024-12-02 15:21:22 +01:00
Giacomo Cusinato
71b11ed829 feat: add donate footer to updater dialog 2024-12-02 15:21:22 +01:00
dependabot[bot]
3aedafa306 build(deps): Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-29 01:38:27 -08:00
dependabot[bot]
284dd83d7d build(deps): Bump peter-evans/create-pull-request from 5 to 7
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 7.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v7)

---
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>
2024-11-29 00:05:37 -08:00
per1234
c09b5f718a Use Ubuntu 18.10 in Linux build container
Background
==========

Shared Library Dependencies
---------------------------

The Linux build of Arduino IDE has dynamic linkage against the libstdc++ and glibc shared libraries. This results in
it having a dependency on the version of the libraries that happens to be present in the environment it is built in.

Although newer versions of the shared libraries are compatible with executables linked against an older version, the
reverse is not true. This means that building Arduino IDE on a Linux machine with a recent distro version installed
causes the IDE to error on startup for users who have a distro with older versions of the dependencies.

For example, if Arduino IDE were built on a machine with version 3.4.33 of libstdc++, then attempting to run it on a
machine with an older version of libstdc++ would fail with an error like:

```
Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.33' not found (required by /home/foo/arduino-ide/resources/app/lib/backend/native/nsfw.node)
```

Likewise,  if Arduino IDE were built on a machine with version 2.39 of glibc, then attempting to run it on a machine
with an older version of glibc would fail with an error like:

```
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by /home/foo/arduino-ide/resources/app/node_modules/nsfw/build/Release/nsfw.node)
```

Build Machine Requirements
--------------------------

The IDE builds distributed by Arduino should be compatible with a reasonable range of Linux distribution versions. In
order to achieve this, the builds must be performed in a machine with an older version of the shared libraries. The
shared libraries are part of the Linux distro, and installing a different version is not feasible. So this imposes a
maximum limit on the build machine's distro version.

The distributed builds are generated via a GitHub Actions workflow. The most simple approach is to run the build in the
machine of the GitHub-hosted runners provided for each operating system. However, GitHub provides a limited range of
operating system versions in their runners, and removes the older versions as newer versions are added. This means that
building in the GitHub-hosted runner machine would not allow for the desired range of Linux distro version
compatibility. For this reason, the Linux build is performed in a Docker container that provides an older version of
Ubuntu.

The same situation of incompatibility with Linux distro versions that have a version of the shared library dependencies
older than the version present on the build machine occurs for several of the tools and frameworks used by the build
process (e.g., Node.js, Python). In this case, the tables are turned as we are now the user rather than the distributor
and so are at the mercy of the Linux distro version compatibility range provided by the distributor. So this imposes a
minimum limit on the build machine's distro version.

Although several of the dependencies used by the standard build system have dependencies on versions of glibc higher
than the version 2.27 present in Ubuntu 18.04, it was possible to use this distro version in the Linux build container
by using alternative distributions and/or versions of these dependencies.

Workflow Artifacts
------------------

The build workflow uses GitHub actions workflow artifacts to transfer the files generated by the build job to subsequent
jobs in the workflow. The "actions/upload-artifact" action is used for this purpose.

Problem
=======

GitHub is dropping support for the workflow artifacts produced by the version 3.x of the "actions/upload-artifact"
action that was previously used by the build job. So the action version used in the build workflow was updated to the
current version 4.x. This version of the action uses a newer version of the Node.js runtime (20). Unfortunately the the
Node.js 20 runtime used by the action has a dependency on glibc version 2.28, which causes the Linux build job to fail
after the update of the "actions/upload-artifact" action:

```
Run actions/upload-artifact@v4
/__e/node20/bin/node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /__e/node20/bin/node)
```

Unlike the other dependencies of the build process, it is no longer possible to work around this incompatibility by
continuing to use the older compatible version of the "actions/upload-artifact" action. It is also impossible to replace
the incompatible Node.js 20.x distribution used by the action, since it comes from the read-only file system of the
runner image. Likewise, it is not possible to configure or force the action to use a Node.js installation at a different
path on the runner machine.

Resolution
==========

Compatibility with the new version of the "actions/upload-artifact" action is attained by updating the version of Linux
in the build container to 18.10, which is the oldest version that has glibc 2.28. The presence of a newer glibc version
in the container also makes it compatible with several other dependencies of the build process, meaning the code in the
Dockerfile and workflow for working around the incompatibilities of Ubuntu 18.04 can be removed.

Consequences
============

Unfortunately this means the loss of compatibility of the Linux Arduino IDE builds with distros that use glibc 2.27
(e.g., Ubuntu 18.04). User of those distros will now find that Arduino IDE fails to start with an error like:

```
Error: node-loader:
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /home/foo/arduino-ide/resources/app/lib/backend/native/pty.node)
    at 85467 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:2766)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 23571 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:3374073)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 55444 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:3369761)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 24290 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:1780542)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
    at 43416 (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:1770138)
    at __webpack_require__ (/home/foo/arduino-ide/resources/app/lib/backend/main.js:2:6663105)
```
2024-11-27 06:58:10 -08:00
per1234
dba57b312c Don't upload multiple times to same artifact in build workflow
The build workflow produces binaries for a range of target hosts. This is done by using a job matrix in the GitHub
Actions workflow that produces each build in a parallel job. GitHub Actions workflow artifacts are used to transfer the
generated files between sequential jobs in the workflow. The "actions/upload-artifact" action is used for this purpose.

Previously, a single artifact was used for this purpose, with each of the parallel jobs uploading its own generated
files to that artifact. However, support for uploading multiple times to a single artifact was dropped in version 4.0.0
of the "actions/upload-artifact" action. So it is now necessary to use a dedicated artifact for each of the builds.
These can be downloaded in aggregate by using the artifact name globbing and merging features which were introduced in
version 4.1.0 of the "actions/download-artifact" action.
2024-11-27 06:58:10 -08:00
per1234
90d3d77ca4 Don't upload multiple times to same artifact in label sync workflow
The "Sync Labels" GitHub Actions workflow is configured to allow the use of multiple shared label configuration files.
This is done by using a job matrix in the GitHub Actions workflow to download each of the files from the source
repository in a parallel GitHub Actions workflow job. A GitHub Actions workflow artifact was used to transfer the
generated files between sequential jobs in the workflow. The "actions/upload-artifact" and "actions/download-artifact"
actions are used for this purpose.

Previously, a single artifact was used for the transfer of all the shared label configuration files, with each of the
parallel jobs uploading its own generated files to that artifact. However, support for uploading multiple times to a
single artifact was dropped in version 4.0.0 of the "actions/upload-artifact" action. So it is now necessary to use a
dedicated artifact for each of the builds. These can be downloaded in aggregate by using the artifact name globbing and
merging features which were introduced in version 4.1.0 of the "actions/download-artifact" action.
2024-11-27 06:58:10 -08:00
dependabot[bot]
0aec778e84 build(deps): Bump geekyeggo/delete-artifact from 2 to 5
Bumps [geekyeggo/delete-artifact](https://github.com/geekyeggo/delete-artifact) from 2 to 5.
- [Release notes](https://github.com/geekyeggo/delete-artifact/releases)
- [Changelog](https://github.com/GeekyEggo/delete-artifact/blob/main/CHANGELOG.md)
- [Commits](https://github.com/geekyeggo/delete-artifact/compare/v2...v5)

---
updated-dependencies:
- dependency-name: geekyeggo/delete-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
dependabot[bot]
84d2dfd13e build(deps): Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
dependabot[bot]
86c7fd7b59 build(deps): Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 06:58:10 -08:00
Giacomo Cusinato
de265694ee feat: use Arduino CLI v1.1.1 2024-11-27 14:05:16 +01:00
Giacomo Cusinato
8462d8a391 refactor: generate-protocol now fetch proto files from arduino_cli_{version}_proto.zip
- Use the CLI release proto.zip to get proto files for production versions of CLI
- Extract the proto files from repo if CLI version is declared as `commitsh` or version is 1.1.0

See https://github.com/arduino/arduino-cli/pull/2761
2024-11-27 14:05:16 +01:00
dankeboy36
48d6d37539 feat: can skip verify before upload
Adds a new preference to control whether the
verify command should automatically run before the
upload. If the `arduino.upload.autoVerify` setting
value is `false`, IDE does not recompile the
sketch code before uploading it to the board.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2024-11-26 21:41:48 +01:00
Giacomo Cusinato
d1065886ef feat: use Arduino CLI 1.1.0 2024-11-21 14:21:30 +01:00
Giacomo Cusinato
8773bd67ab fix: use missing google proto files in CLI 2024-11-21 14:21:30 +01:00
Giacomo Cusinato
4189b086de fix: update yarn.lock 2024-11-21 10:44:20 +01:00
dankeboy36
3fc8474d71
fix: align viewsWelcome behavior to VS Code (#2543)
* fix: align `viewsWelcome` behavior to VS Code

Ref: eclipse-theia/theia#14309
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

* fix: update change proposal from Theia as is

Ref: arduino/arduino-ide#2543
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2024-11-21 08:43:04 +01:00
Giacomo Cusinato
4cf9909a07
fix: retry compilation if grpc client needs to be reinitialized (#2548)
* fix: use `Status` enum for status code in `ServiceError` type guards

This change resolves the issue where the intersection of `ServiceError` error codes of type `number` resulted in the `never` type due to conflict between number and `State` enum if `StatusObject`

* feat: add `isInvalidArgument` type guard to `ServiceError`

* fix: retry compilation if grpc client needs to be reinitialized

See https://github.com/arduino/arduino-ide/issues/2547
2024-11-21 08:42:14 +01:00
Giacomo Cusinato
41844c9470
feat: implement menu action to reload current board data (#2553) 2024-11-21 08:41:26 +01:00
Giacomo Cusinato
7c231fff76
fix: memory leak when scanning sketchbooks with large files (#2555)
Resolves https://github.com/arduino/arduino-ide/issues/2537

Fix memory leak issue caused by inflight dependency, see https://github.com/isaacs/node-glob/issues/435
2024-11-21 08:40:52 +01:00
dependabot[bot]
d6235f0a0c build(deps): Bump svenstaro/upload-release-action from 2.7.0 to 2.9.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.7.0 to 2.9.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.7.0...2.9.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 04:04:35 -08:00
per1234
d377d00042 Use appropriate equality operator in changelog script
It is considered good practice to use JavaScript's type-safe strict equality operator === instead of the equality
operator ==. Compliance with this practice is enforced by the project's ESLint configuration, via the "eqeqeq" rule.

The script used to generate the changelog for Arduino IDE's auto-update dialog contained an inappropriate usage of the
equality operator. This caused linting runs to fail:

arduino-ide-extension/scripts/compose-changelog.js
  37:19  error  Expected '===' and instead saw '=='  eqeqeq
2024-11-19 03:53:36 -08:00
per1234
f232010bec Correct eslint command in lint script
The `lint` script of the "arduino-ide-extension" package is intended to use the ESLint linter tool to check for problems
in all the package's JavaScript and TypeScript code files. It is used by the continuous integration system to validate
contributions.

Previously, the command invoked `eslint` without any arguments. With the 8.x version of ESLint used by the project, it
is necessary to provide a path argument in order to cause it to lint the contents of files. Because that argument was
not provided, the script didn't do anything at all and so would return a 0 exit status even if the code had linting rule
violations.

This is fixed by adding a `.` path argument to the command invoked by the script. This will cause ESLint to recurse
through the `arduino-ide-extension` folder and lint the code in all relevant files.
2024-11-19 03:53:36 -08:00
per1234
788017bb99 Use a dedicated GitHub workflow to check for problems with Yarn configuration
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to check the sync of the Yarn lockfile.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Makes the build workflow more difficult to maintain
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The sync check operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
9331d2ec0d Use a dedicated GitHub Actions workflow for testing TypeScript/JavaScript code
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to perform the unrelated operation of running the project's
test suites.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Unnecessarily adds a significant amount of extra content to the already extensive logs produced by the build process
* Makes the build workflow more difficult to maintain
* Increases the length of a build workflow run
* Increases the impact of a spurious failure
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The test run operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
6e695429cc Use a dedicated GitHub Actions workflow for linting TypeScript/JavaScript code
The "build" workflow builds the application for all supported targets, generates workflow artifacts from which the
builds can be downloaded by users and beta testers, and publishes nightly and production releases.

As if that wasn't enough, the workflow was also configured to perform the unrelated operation of linting the project's
TypeScript and JavaScript code.

This monolithic approach is harmful for multiple reasons:

* Makes it difficult to interpret a failed workflow run
* Unnecessarily adds a significant amount of extra content to the already extensive logs produced by the build process
* Makes the build workflow more difficult to maintain
* Increases the length of a build workflow run
* Increases the impact of a spurious failure
* Increases the turnaround time for contributors and maintainers to get feedback from the CI system

The linting operation is hereby moved to a dedicated workflow, consistent with standard practices for Arduino Tooling
projects.
2024-11-18 06:57:16 -08:00
per1234
4f8b9800a0 Remove redundant signing determination code from build system
The "build" workflow signs the macOS and Windows builds of the application. The signing process relies on access to GitHub Actions
secrets. For this reason, the workflow is configured to only sign the builds when it has access to GitHub Actions
secrets to avoid spurious failures of the workflow that would otherwise be caused by signing failure.

A flexible general purpose system for determining whether to attempt signing of a build was established years ago. However, a redundant system was added specific to the Windows build instead of using the existing system.

The redundant system is hereby removed. This makes the workflow easier to understand and maintain.
2024-11-17 22:00:34 -08:00
per1234
f72d1f0ac8 Use appropriate indicator for Windows signing determination in build workflow
The "build" workflow signs the Windows builds of the application. The signing process relies on access to GitHub Actions
secrets. For this reason, the workflow is configured to only sign the builds when it has access to GitHub Actions
secrets to avoid spurious failures of the workflow that would otherwise be caused by signing failure.

Previously the signing was determined based on the value of the `github.event.pull_request.head.repo.fork` context item.
That was effective for the use case of the workflow being triggered by a pull request from a fork (for security reasons,
GitHub Actions does not give access to secrets under these conditions).

However, there is another context under which the workflow might run without access to the signing secrets, for which
the use of context item is not appropriate. It is important to support the use of the workflow in forks of the
repository. In addition to the possible value to hard forked projects, this is essential to allow conscientious
contributors to test contributions to the build and release system in their own fork prior to submitting a pull request.
The previous configuration would cause a workflow run performed by a contributor in a fork to attempt to sign the
Windows build. Unless the contributor had set up the ridiculously complex infrastructure required to perform the signing
for the Windows build, which is utterly infeasible, this would cause the workflow to fail spuriously.

The appropriate approach, which has been the established convention in the rest of the workflow code, is to use the
secret itself when determining whether to attempt the signing process. If the secret is not defined (resulting in it
having an empty string value), then the signing should be skipped. If it is defined, then the signing should be
performed.
2024-11-17 22:00:34 -08:00
per1234
0fe0feace4 Get job-specific configuration from matrix in build workflow
The "build" workflow builds the application for a range of target hosts. This is done by using a job matrix. A separate
parallel job runs for each target. The target-specific configuration data is defined in the job matrix array.

This configuration data includes the information related to the code signing certificates. Inexplicably, during the work
to add support for signing the Windows builds with an "eToken" hardware authentication device, this data was not used
for the Windows code signing configuration. Instead the certificate data was redundantly hardcoded into the workflow
code.

The Windows code signing certificate configuration is hereby changed to use the established flexible job configuration
data system. This makes the workflow easier to understand and maintain.
2024-11-17 20:18:52 -08:00
per1234
43f0ccb250 Use appropriate indicator for dependency installation conditionals in build workflow
The Windows builds of the application are cryptographically signed. The signing requires an "eToken" hardware
authentication device be connected to the machine performing the signing. This means that it is necessary to use a
self-hosted GitHub Actions runner for the Windows job of the build workflow rather than the runners hosted by GitHub.

There are some unique characteristics of the self-hosted runner which the workflow code must accommodate. One of these
is that, rather than installing dependencies of the build process during the workflow run as is done for the
GitHub-hosted runners, the dependencies are preinstalled in the self-hosted runner machine. So the dependency
installation steps must be configured so that they will be skipped when the job is running on the self-hosted runner.

This is done by adding a conditional to the steps. Previously the conditional was based on the value of the `runner.os`
context item. This is not an appropriate indicator of the job running on the self-hosted runner because `runner.os` will
have the same value if the job was running on a GitHub-hosted Windows runner. That might seem like only a hypothetical
problem since the workflow does not use a GitHub-hosted Windows runner. However, it is important to support the use of
the workflow in forks of the repository. In addition to the possible value to hard forked projects, this is essential to
allow conscientious contributors to test contributions to the build and release system in their own fork prior to
submitting a pull request.

The conditionals are changed to use the more appropriate indicator of the specific name of the self-hosted Windows
runner (via the `runner.name` context item).
2024-11-17 03:15:42 -08:00
per1234
c0b0b84d79 Simplify and generalize configurable working directory code in build workflow
The Windows builds of the application are cryptographically signed. The signing requires an "eToken" hardware
authentication device be connected to the machine performing the signing. This means that it is necessary to use a
self-hosted GitHub Actions runner for the Windows job of the build workflow rather than the runners hosted by GitHub.

There are some unique characteristics of the self-hosted runner which the workflow code must accommodate. The default
working directory of the self-hosted runner is not suitable to perform the build under because the the resulting folder
structure produced paths that exceeded the ridiculously small maximum path length of Windows. So the workflow must be
configured to use a custom working directory with a short path (`C:\a`).

This custom working directory must be used only for the job running on the self-hosted Windows runner so the working
directory of the relevant workflow steps are configured using a ternary expression. Previously, this expression had
multiple conditions:

* the value of the `runner.os` context item
* the definition of a custom working directory value in the job matrix

The second condition is entirely sufficient. The use of the first condition only added unnecessary complexity to the
workflow code, and imposed a pointless limitation of only allowing the use of the custom working directory system on
Windows runners.

Removing the unnecessary condition makes the workflow easier to understand and maintain, and makes it possible to
configure any job to use a custom working directory if necessary.
2024-11-17 02:15:21 -08:00
per1234
3d82cb3525 Add PAID_RUNNER_BUILD_DATA environment variable back to build workflow
The build workflow produces builds for a range of target host architectures, including macOS Apple Silicon. This is done
by running a native build in a machine of the target architecture.

At the time the support for producing Apple Silicon builds was added to the workflow, use of GitHub-hosted Apple Silicon
runner machines was charged by the minute (while use of the other runners is free). In order to avoid excessive
expenses, the workflow was configured so that the Apple Silicon builds were only produced when absolutely necessary.
This was done by defining two sets of job matrix arrays, one for jobs using free runners, and the other for jobs using
paid runners. Due to the limitations of the GitHub Actions framework, it was necessary to use workflow environment
variables for this purpose.

Since that time, GitHub made free GitHub-hosted Apple Silicon runners available. When the workflow was adjusted to use
that runner, the configuration for the Apple Silicon build job was moved to the free runner job matrix array. The system
for supporting selective use of paid GitHub-hosted runners to produce builds was not removed at that time. This is
reasonable since it is possible the need will arise for using paid runners at some point in the future (e.g., only
legacy older versions of free macOS "Intel" runners are now provided and it is likely that even these will eventually be
phased out forcing us to use the paid runner to produce builds for that target). However, the environment variable for
the paid runner job matrix array data was removed. The absence of that variable made it very difficult to understand the
workflow code for the system.

For this reason, the environment variable is replaced, but empty of data. A comment is added to explain the reason for
this.
2024-11-17 00:54:17 -08:00
per1234
9cbee0eacf Trim trailing whitespace in build workflow 2024-11-17 00:54:17 -08:00
Giacomo Cusinato
63e9dfd7f5 fix: disable local windows signing for forks PR
Resolves https://github.com/arduino/arduino-ide/issues/2545
2024-11-11 14:53:42 +01:00
dankeboy36
3ccc864453
fix(doc): add missing prerequisites to dev docs (#2531)
Document prerequisites of the Arduino CLI, LS, etc. tools when built
from a Git commitish.

---------

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
Co-authored-by: per1234 <accounts@perglass.com>
2024-10-26 17:15:23 -07:00
Dave Simpson
44f15238d6
chore: switch to version 2.3.4 after the release (#2514) 2024-10-24 09:26:49 +02:00
Giacomo Cusinato
4a3abf542c fix: prevent parsing CLI errors without metadata
When parsing a CLI error, check if any metadata from grpc is present before trying to parse it.
Closes #2516
2024-10-22 09:06:28 -07:00
per1234
91bb75ca97 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.
2024-10-22 07:52:48 -07:00
Dave Simpson
77136687d3
Use macos-latest runner for macOS ARM build (#2513) 2024-09-24 18:30:58 +02:00
github-actions[bot]
16bc1a4610
Updated translation files (#2392)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-09-24 17:05:43 +02:00
Dave Simpson
2921979678
Use macos-13 for Intel build (#2508) 2024-09-24 15:59:16 +02:00
Giacomo Cusinato
a5bf56ffa6 feat: upload using programmer by default if board requires it 2024-09-19 11:57:42 +02:00
Giacomo Cusinato
2de8bd1717 feat: decode grpc status objects and map them to protocol types
Status object thrown by grpc commands contains metadata that needs to be serialized in order to map it to custom errors generated through proto files https://github.com/grpc/grpc-web/issues/399
2024-09-19 11:57:42 +02:00
Giacomo Cusinato
1ec0a8cc77
feat: use Arduino CLI 1.0.4 (#2457)
* fix: use `@pingghost/protoc` to compile proto files

The npm package previously used (`protoc`) is still lacking apple arm32 support, see https://github.com/YePpHa/node-protoc/pull/10

* feat: use Arduino CLI 1.0.4

* fix: allow use of node16 in github actions

* chore: update `arduino-language-server` version for cli-1.0.0

* fix: deprecated platform order test

Arduino deprecated platforms should have more priority then other deprecated ones
2024-09-06 11:38:55 +02:00
Giacomo Cusinato
c3adde5460
feat: add shared space support (#2486) 2024-09-06 10:29:31 +02:00
Dave Simpson
2e78e96b75
[chore] Update Windows signing Cert to eToken (#2452) 2024-07-03 09:42:10 +02:00
Akos Kitta
aa9b10d68e fix: copy example with .pde main sketch file
Closes arduino/arduino-ide#2377

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-22 16:19:10 +01:00
Akos Kitta
4217c0001d fix: add missing installed version to the platform
Closes arduino/arduino-ide#2378

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-21 17:01:28 +01:00
Akos Kitta
a088ba99f5 fix: invalid custom board option handling in FQBN
Closes arduino/arduino-ide#1588

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
2a325a5b74 feat: cancelable verify+upload
Closes arduino/arduino-ide#1199

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
347e3d8118 fix: can unset network#proxy in the CLI config
An empty object (`{}`) must be used to correctly unset the CLI config
value to its default.

Closes arduino/arduino-ide#2184

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
8e09971078 feat: use Arduino CLI 0.36.0-rc.1 APIs
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 13:38:52 +01:00
Akos Kitta
48e7bf6b5d chore: switch to version 2.3.3 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-20 12:55:39 +01:00
Akos Kitta
95c4399c07 fix(ci): use go 1.21 for the on the fly bin builds
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:37:27 +01:00
Akos Kitta
4a807ab538 fix: no required programmer for debug --info
Ref: arduino/arduino-cli#2540
Closes: arduino/arduino-ide#2368

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:37:27 +01:00
Akos Kitta
1a98485b02 fix(security): use ip@2.0.1 for CVE-2023-42282
Refs:
 - https://github.com/advisories/GHSA-78xj-cgh5-2h22
  - 32f468f124
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-19 17:02:23 +01:00
github-actions[bot]
8fe6a81230 Updated translation files 2024-02-19 11:40:19 +01:00
Akos Kitta
547a630598 chore: switch to version 2.3.2 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-15 10:27:29 +01:00
github-actions[bot]
ff8c646cfa Updated translation files 2024-02-14 13:35:47 +01:00
Akos Kitta
316e0fd8be fix(security): update all vulnerable dependencies
Resolutions:
 - `nano@^10.1.3`,
 - `msgpackr@^1.10.1`,
 - `axios@^1.6.7`

Fixes:
 - [GHSA-wf5p-g6vw-rhxx](https://github.com/advisories/GHSA-wf5p-g6vw-rhxx) (`CVE-2023-45857`)
 - [GHSA-jchw-25xp-jwwc](https://github.com/advisories/GHSA-jchw-25xp-jwwc) (`CVE-2023-26159`)
 - [GHSA-7hpj-7hhx-2fgx](https://github.com/advisories/GHSA-7hpj-7hhx-2fgx) (`CVE-2023-52079`)

Ref: nrwl/nx#20493
Ref: eclipse-theia/theia#13365

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-14 09:31:44 +01:00
Akos Kitta
ca779e5cf2 fix: debug widget layout updates
Use customized `PanelLayout#removeWidget` and
`PanelLayout#insertWidget` logic for the layout
updates. The customized functions ensure no side
effect if adding/removing the widget to/from the
layout but it's already present/absent.

Unlike the default [`PanelLayout#removeWidget`](9f5e11025b/packages/widgets/src/panellayout.ts (L154-L156))
behavior, do not try to remove the widget if it's
not present (has a negative index). Otherwise,
required widgets might be removed based on the
default [`ArrayExt#removeAt`](9f5e11025b/packages/algorithm/src/array.ts (L1075-L1077))
behavior.

Closes: arduino/arduino-ide#2354

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-08 14:17:10 +01:00
per1234
74c580175b Use arduino/setup-task@v1 action for Linux build job
Unfortunately the latest v2 version of the arduino/setup-task action used to install the Task task runner tool in the
runner machine has a dependency on a higher version of glibc than is provided by the Linux container. For this reason,
the workflow is configured to use arduino/setup-task@v1 for the Linux build job.

We will receive pull requests from Dependabot offering to update this outdated action dependency at each subsequent
major version release of the action (which are not terribly frequent). We must decline the bump of the action in that
specific step, but we can accept the bumps of all other usages of the action in the workflows. Dependabot remembers when
you decline a bump so this should not be too bothersome.
2024-02-07 20:40:49 -08:00
dependabot[bot]
71bd189eb1 build(deps): Bump arduino/setup-task from 1 to 2
Bumps [arduino/setup-task](https://github.com/arduino/setup-task) from 1 to 2.
- [Release notes](https://github.com/arduino/setup-task/releases)
- [Commits](https://github.com/arduino/setup-task/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 20:40:49 -08:00
Akos Kitta
0822ed28da chore: switch to version 2.3.1 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-07 19:05:11 +01:00
Akos Kitta
1b9c7e93e0 chore: use version 2.3.0 for the next release
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-07 13:09:34 +01:00
Akos Kitta
d419a6c6f0 feat: preference to limit thread count of the LS
Added a new preference (`arduino.language.asyncWorkers`) to control the
number of async workers used by `clangd`.
Users can fine tune the `clangd` thread count to overcome the excessive
CPU usage.

Use 0.1.2 Arduino Tools VSIX in IDE2.

Ref: arduino/arduino-language-server#177
Ref: arduino/vscode-arduino-tools#46

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-07 12:28:57 +01:00
github-actions[bot]
dda7770105 Updated translation files 2024-02-01 11:29:45 +01:00
Akos Kitta
763fde036c feat: disable debug widget if unsupported by board
Remove the 'Add configuration...' select option from the debug widget.

Closes #14

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-31 12:04:29 +01:00
per1234
0e7b0c9486 Bundle native Arduino Firmware Uploader with Apple Silicon build
A separate build of Arduino IDE is produced for each of the macOS host architectures:

* Apple Silicon (ARM)
* Intel (x86)

The Arduino IDE distribution includes several bundled helper tools. The build of those tools should also be selected according to the target host architecture.

At the time the infrastructure was set up for bundling the "Arduino Firmware Uploader" tool with the Arduino IDE distribution, that tool was only produced in a macOS x86 variant. So this was used even in the Apple Silicon Arduino IDE distribution, relying on Rosetta 2 to provide compatibility when used on Apple Silicon machines. Since that time, the Arduino Firmware Uploader build infrastructure has been updated to produce an Apple Silicon build of the tool and such a build is available for the IDE's current version dependency of Arduino Firmware Updater.

The "Arduino Firmware Uploader" tool bundling script is hereby updated to use the most appropriate variant of the dependency for the target host architecture. This provides the following benefits

* Compatibility with systems that don't have Rosetta 2 installed
* Improved performance
2024-01-22 02:32:45 -08:00
Akos Kitta
b8dd39c729 chore: pinned Arduino CLI 0.35.1
Closes arduino/arduino-ide#2318

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-17 17:30:25 +01:00
Akos Kitta
d6de53780d chore(dev): use latest settings for auto-format
From VS Code `>=1.85`

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-17 17:30:25 +01:00
per1234
2f2b19f613 Bump built-in example sketches version to 1.10.1
The Arduino IDE installation includes a collection of example sketches demonstrating fundamental concepts.

These examples are hosted in a dedicated repository, which is a dependency of this project. A new release has been made
in that `arduino/arduino-examples` repository.

The infrastructure for downloading the examples during the Arduino IDE build is hereby updated to use the latest release
of the `arduino/arduino-examples` repository.
2024-01-15 00:40:18 -08:00
Akos Kitta
0ca1a31747 fix: board <select> update on board detach
When the previously selected board is not detected, unset the `<select>`
option.

Closes #2222

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-15 09:25:10 +01:00
Akos Kitta
d01f95647e fix: sketch Save As errors on name collision
The Save As operation is halted and the problem clearly communicated to
the user when there is a collision between the target primary source
file name and existing secondary source files of the sketch.

Closes #827

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-15 09:24:50 +01:00
Akos Kitta
074f654457 fix: sketch Save As preserves the folder structure
This commit rewrites how IDE copies sketches as part of the _Save As_
operation. Instead of copying to the destination, IDE copies the sketch
into a temporary location, then to the desired destination.

This commit drops [`cpy`](https://www.npmjs.com/package/cpy).
Ref: 47b89a70b5

Closes #2077

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-15 09:24:50 +01:00
Akos Kitta
3eef857b48 fix: can open IDE from an ino file
Use a fallback frontend app config when starting IDE2 from an ino file
(from Explorer, Finder, etc.) and the app config is not yet set.

Closes #2209

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-01-15 09:16:11 +01:00
Akos Kitta
73b6dc4774 feat: use new debug -I -P CLI output
- Can pick a programmer if missing,
 - Can auto-select a programmer on app start,
 - Can edit the `launch.json`,
 - Adjust board discovery to new gRPC API. From now on, it's a client
 read stream, not a duplex.
 - Allow `.cxx` and `.cc` file extensions. (Closes #2265)
 - Drop `debuggingSupported` from `BoardDetails`.
 - Dedicated service endpoint for checking the debugger.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
42bf1a0e99 test: test Arduino state update for extensions
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
5abdc18fcc fix: make hosted plugin support testable
Hide the concrete implementation behind an interface so that tests can
`require` it.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
633346a3b0 feat: new window inherits the custom board options
A new startup task ensures setting any custom board menu selection in a
new sketch window.

Closes #2271

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
0f83a48649 chore(deps): update to electron@27.0.3
- Related change: 153e34f11b
 - Reported at: https://github.com/arduino/arduino-ide/pull/2267#issuecomment-1795180432
 - External: https://forum.arduino.cc/t/ide-2-2-1-main-window-randomly-goes-blank/1166219

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
a0bd5d022f chore: use 0.7.5 Arduino LS
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
101ba650f3 feat: handle v prefix in CLI GH release name
Ref: arduino/arduino-cli#2374
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
Akos Kitta
64ce35edbb feat: show in tooltip if core is from sketchbook
Closes #2270

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-12-13 17:32:07 +01:00
per1234
2dae4c8258 Remove version pin of Git dependency in Linux build container Dockerfile
A Docker container is used to produce the Linux build of Arduino IDE.

The dependencies of the build are pinned to a specific version in the Dockerfile in order to ensure a stable environment
in the images.

One such dependency is Git. The version of Git available from the package repository of the Ubuntu 18.04 distro used for
the image is too outdated to be used for this purpose. For this reason, a PPA is used as the source for the Git package.
Unfortunately the maintainers of the PPA remove the previous package every time they add a package for a newer version
of Git. This breaks the version pinned installation of the Git package:

5.515 E: Version '1:2.42.0-0ppa1~ubuntu18.04.1' for 'git' was not found

For this reason it is necessary to unpin Git in the Dockerfile. This will cause whichever version is available from the
PPA to be installed each time the image is built. Although not ideal for this application, that shouldn't cause any
problems in practice since Git has quite a stable and carefully maintained interface.
2023-12-10 23:36:50 -08:00
per1234
e7754b7c3b Use actions/setup-go@v4 for Linux build job
Unfortunately the latest v5 version of the actions/setup-go action used to set up the Go programming language in the
runner machine has a dependency on a higher version of glibc than is provided by the Linux container. For this reason,
the workflow is configured to use actions/setup-go@v4 for the Linux build job. We will receive pull requests from
Dependabot offering to update this outdated action dependency for at each subsequent major version release of the action
(which are not terribly frequent). We must decline the bump of the action in that specific step, but accept the bumps of
all other usages of the action in the workflows. Dependabot remembers when you decline a bump so this should not be too
bothersome.
2023-12-06 21:49:28 -08:00
dependabot[bot]
3d2511194a build(deps): Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
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-12-06 21:49:28 -08:00
dependabot[bot]
59a3c4faf0 build(deps): Bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-06 20:35:14 -08:00
Akos Kitta
22a69f7488 chore(deps): update vulnerable dependencies
- Forced the resolution of `@babel/traverse@7.23.2` brought in by
`@theia/cli`. (eclipse-theia/theia#13024)
- Updated to `auth0-js@9.21.3` to transitively pull `crypto-js@4.2.0` in
with the security fixes.

GitHub Advisory Database refs:
 - https://github.com/advisories/GHSA-67hx-6x53-jw92
 - https://github.com/advisories/GHSA-xwcq-pm8m-c4vf

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-11-09 11:32:37 +01:00
dependabot[bot]
503533d712 build(deps): Bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 14:09:12 -07:00
Akos Kitta
8687e2e044 chore(doc): update main screenshot in the readme
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-10-23 07:42:29 +02:00
per1234
69b73657b6 Simplify expression in container conditional steps of build workflow
The "Arduino IDE" GitHub Actions workflow uses a Docker container for the build job of certain target, while running others directly in the
runner environment.

Due to differences between these two environments, some steps must run only when a container is used by the job, and others only when the job is running in the runner environment. This is done by a conditional on the job matrix data regarding the container. The container value is set to null for the jobs that run in the runner environment, while containing a full container configuration mapping for the jobs that run in a container.

Previously the conditional unnecessarily used the value of the image key of the container object specifically. The comparison
can be done against the container value itself. Doing this will make it a little easier to understand the workflow code, since the conditional more clearly reflects the matrix (where `container` is set to `null` instead of `container.image`).
2023-10-19 06:40:46 -07:00
per1234
7e8f723df3 Pin Python version at 3.11 in build workflow
Python 3.12.x is incompatible with the current version of node-gyp (9.4.0). For this reason, it is necessary to
configure the "GitHub Actions" workflow to use the compatible Python 3.11.x until the next release of node-gyp (which
should contain a fix for the breakage) is made.
2023-10-19 06:40:46 -07:00
per1234
d19778d0fb Remove obviated build workflow step for removing Linux channel file
In addition to the builds, when a nightly or production release is published, a "channel update info file" is also
uploaded to Amazon S3 by the "Arduino IDE" GitHub Actions workflow. The IDE checks the channel file on the server to get
information about available updates.

Previously the Linux production release builds were being remade manually due to the ones produced by GitHub Actions not
being compatible with older distro versions due to a dynamically linked dependency. For this reason, a step was
temporarily added to the workflow to cause it to not upload the Linux channel file in order to avoid Linux users from
receiving an update offer before the limited compatibility automated build had been replaced with the manually produced
build.

The automated build system has been adjusted to produce Linux builds with the intended range of compatibility, but the
step that deleted the channel file was not removed at that time. The obviated step is hereby removed in order to allow
complete releases to be published by the workflow.
2023-10-19 06:40:46 -07:00
per1234
e5ef564817 Correct conditional logic for publishing steps of build workflow
The "Arduino IDE" GitHub Actions workflow is used to generate several distinct types of builds:

- Tester builds of commits
- Nightly builds
- Release builds

Different actions must be performed depending on which type of build is being produced. The workflow uses dedicated jobs
for publishing the nightly builds and for publishing the release builds. Those jobs are configured to run only when
certain criteria are met.

One such criteria is that the merge-channel-files job ran as expected. There are four possible result types of a job,
which should be handled as follows:

| Result   | Run dependent job? |
| -------- | ------------------ |
| success  | Yes                |
| failure  | No                 |
| canceled | No                 |
| skipped  | Yes                |

GitHub Actions automatically takes the desired action regarding whether the dependent job should run for the first three
result types, but that is not the case for the "skipped" result. The merge-channel-files job dependency is skipped when
a channel file merge is not needed and so this is not cause to cancel the build publishing.

The only way to make a dependent job run when the dependency was skipped is to add the `always()` expression to the job
conditional. This goes too far in the other direction by causing the job to run even when the dependency failed or was
canceled. So it is necessary to also add logic for each of the dependency job result types to the conditional, which
makes it quite complex when combined with the logic for the other criteria of the job.

In order to reduce the amount of complexity of the conditionals of the dependent jobs, a job was interposed in the
dependency chain, which was intended to act simply as a container for the logic about the merge-channel-files job
result. Unfortunately it turns out that even if the direct dependency job's result was success, if any ancestor in the
dependency chain was skipped, GitHub Actions still skips all dependent jobs without an `always()` expression in their
conditional, meaning the intermediate job was pointless. This caused the build publishing jobs to be skipped under the
conditions where they should have ran.

The pointless intermediate job is hereby removed, an `always()` expression added to the conditionals of the dependent
jobs, and the full logic for how to handle each dependent job result type added there as well.
2023-10-19 06:40:46 -07:00
Lukas
ce01351bfe Update README.md
Co-authored-by: per1234 <accounts@perglass.com>
2023-10-16 01:44:26 -07:00
Lukas
a110c2b1f2 adding a hyperlink to the README 2023-10-16 01:44:26 -07:00
Akos Kitta
153e34f11b chore(deps): update dependencies
To fix all security vulnerabilities detected by `Dependabot`.

 - remove `shelljs`. replace with `fs` and `console`.
 - remove `uuid`. replace with `@phosphor/coreutils`.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-10-13 08:50:39 +02:00
per1234
ed1cb6bcf9 Make Linux build in a container for compatibility with older distros
Background
----------

The Linux build of Arduino IDE is dynamically linked against the libstdc++ and glibc shared libraries. This results in
it having a dependency on the version of the libraries that happens to be present in the environment it is built in.

Although newer versions of the shared libraries are compatible with executables linked against an older version, the
reverse is not true. This means that building Arduino IDE on a Linux machine with a recent distro version installed
causes the IDE to error on startup for users who have a distro with older versions of the dependencies. For example:

```
Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.26' not found (required by /home/per/Downloads/arduino-ide_nightly-20231006_Linux_64bit/resources/app/lib/backend/native/nsfw.node)
```

or:

```
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by /home/per/Downloads/arduino-ide_2.0.5-snapshot-90b1f67_Linux_64bit/resources/app/node_modules/nsfw/build/Release/nsfw.node)
```

We were originally able to achieve our targeted range of Linux distro compatibility by running the Linux build in the
ubuntu-18.04 GitHub Actions hosted runner machine. Unfortunately GitHub stopped offering that runner machine. This meant
we were forced to update to using the ubuntu-20.04 runner machine instead, which caused the loss of compatibility of
the automatically generated builds with previously supported distro versions (e.g., Ubuntu 18.04). Since that time, the
release builds have been produced manually, which is inefficient and prone to human error.

Update
------

The identified solution to restoring fully automated builds with the target range of Linux distro compatibility is to
run the build in a Docker container that provides the suitable environment. This means a combination of an older distro
version with the modern versions of the development tool dependencies of the build.

Such a combination was achieved by creating a bespoke image based on the ubuntu:18.04 image. The Dockerfile is hosted in
the Arduino IDE repository in order to allow it to be maintained in parallel with the code and infrastructure.

Image Publishing
----------------

A "Push Container Images" GitHub Actions continuous delivery workflow is added to push updated images to the GitHub
Container registry when a commit that modifies relevant files is pushed to the main branch.

This means the image does not have formally versioned tags and the IDE build uses the container that results from the
configuration at the tip of the main branch at the time of the build. I think that is a reasonable approach in this use
case where the image is targeted to a single application rather than intended to be used by multiple projects.

Container Validation
--------------------

The build workflow is configured to trigger on completion of that push workflow in order to provide validation of the IDE build using the
updated container as well as the resulting tester builds of the IDE.

A "Check Containers" GitHub Actions continuous integration workflow is added to provide basic validation for changes to
the Dockerfile. It will automatically build the image and run the container on any push or pull request that modifies
relevant files.

Container Workflow Design
-------------------------

With the goal of reusability, the image data is contained in a job matrix in the workflow to allow them to accommodate
any number of arbitrary images.

Build Workflow Configuration
----------------------------

A container property is added to the build job matrix data. If the container.image property is set to null, GitHub
Actions will run the job directly in the runner environment instead of in a container. This allows us to produce the
builds using either a container or a bare runner machine as is appropriate for each target.

Unfortunately the latest v4 version of the actions/checkout action used to checkout the repository into the job
environment has a dependency on a higher version of glibc than is provided by the Linux container. For this reason, the
workflow is configured to use actions/checkout@v3 for the Linux build job. We will likely receive pull requests from
Dependabot offering to update this outdated action dependency for the v4 and at each subsequent major version release of
the action (which are not terribly frequent). We must decline the bump of the action in that specific step, but accept
the bumps of all other usages of the action in the workflows. Dependabot remembers when you decline a bump so this
should not be too bothersome.
2023-10-12 11:24:29 -07:00
Akos Kitta
a8e63c8c90 test: relax accessible sketch path test condition
- Do not expect `EACCESS` on Linux.
 - Run the test only if `EACCESS` occurs via the `stat` syscall.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-10-09 19:24:51 +02:00
Akos Kitta
0b2410d49a
test: run cloud sketches tests on the CI (#2092)
- fix(test): integration tests are more resilient.
   - run the Create integration tests with other slow tests,
   - queued `PUT`/`DELETE` requests to make the test timeout happy,
   - reduced the `/sketches/search` offset to 1/5th and
   - remove Create API logging.
 - fix(linter): ignore `lib` folder. Remove obsolete `.node_modules`
pattern.
 - feat(ci): enable Create API integration tests.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
Co-authored-by: per1234 <accounts@perglass.com>
2023-10-07 10:38:54 +02:00
per1234
3f4d2745a8 Use a generalized criterion for S3 publishing determination in build workflow
The "Arduino IDE" GitHub Actions workflow uploads the nightly and release builds to Amazon S3, from which they are
downloaded by the auto-update as well as directly by users via the links on the "Software" page of arduino.cc.

The workflow can also be useful in forks. Either by those who want to test contributions staged in their fork prior to
submitting a PR to the parent repo, or by those maintaining a hard fork of the project. Even though these forks wouldn't
(and couldn't due to lack of access to the encrypted credential secrets only available to the workflow when ran in a
trusted context in Arduino's repo)credentials stored in Arduino's repo) use the S3 upload component of the workflow,
they may still find it valuable for continuous integration as well as continuous deployment via the tester builds and
release builds the workflow also publishes to the GitHub repository it runs in. For this reason, the workflow contains
code to determine whether it should attempt the S3 uploads.

Previously the repository name was used as the criteria in that code. The project specificity of that approach makes the
workflow less easily reusable. A more generally applicable criterion is whether the encrypted credential certificate is
defined.

The new criterion allows the workflow to be used in any repository where the administrator has created an encrypted
secret containing their AWS credentials. That might be other projects owned by Arduino, or even 3rd party projects where
the owners want to take a similar build publishing approach using their own AWS account.
2023-10-06 09:50:52 -07:00
per1234
136545491d Deduplicate S3 publishing determination code in build workflow
The "Arduino IDE" GitHub Actions workflow uploads the nightly and release builds to Amazon S3, from which they are
downloaded by the auto-update as well as directly by users via the links on the "Software" page of arduino.cc.

The workflow can also be useful in forks. Either by those who want to test contributions staged in their fork prior to
submitting a PR to the parent repo, or by those maintaining a hard fork of the project. Even though these forks wouldn't
(and couldn't due to lack of access to the encrypted credential secrets only available to the workflow when ran in a
trusted context in Arduino's repo)credentials stored in Arduino's repo) use the S3 upload component of the workflow,
they may still find it valuable for continuous integration as well as continuous deployment via the tester builds and
release builds the workflow also publishes to the GitHub repository it runs in. For this reason, the workflow contains
code to determine whether it should attempt the S3 uploads. Previously that code was duplicated in both the nightly and
release publishing jobs of the workflow. Since the workflow already contains a job specifically for the purpose of
determining the characteristics of the build being performed and making that information available from single source
for use throughout the rest of the workflow, it makes sense to also move the S3 upload determination code to that job.
2023-10-06 09:50:52 -07:00
per1234
7e1d441e6a Add native Apple Silicon target to build workflow
On every release tag, and manual trigger when the "Include builds on non-free runners" checkbox is checked, make the
Arduino IDE build for native Apple Silicon host in addition to the builds that are always generated by the "Arduino IDE"
GitHub Actions workflow.

Previously, the build workflow only produced a build for x86-64 (AKA "Intel") macOS hosts. Although it is possible to
use those builds on Apple Silicon machines via the Rosetta 2 translation software, the performance is significantly
inferior to a native build so we must also provide Apple Silicon native builds.

Previously the Apple Silicon builds were produced manually. The reason for using that inefficient and error-prone
approach instead of the automated continuous deployment system used for other builds was that GitHub did not provide the
necessary Apple Silicon runner machines and Arduino was not capable of setting up such self-hosted machines in a manner
that would make them feasible for the project maintainers to use. GitHub hosted Apple Silicon runner machines are now
available so we can add the target to the build workflow.

GitHub gives unlimited use of the basic runner machines for workflow runs in public repositories. However, the macOS ARM
architecture is only provided in runner machines which are classified as "larger runner". Use of these runners is
charged on a per-minute basis, without any of the free allowances GitHub provides for the normal runners. In order to
avoid unnecessary expenditures, native Apple Silicon builds must be generated only when there is compelling reason to do
so. Such a build is needed for every release, so the workflow is configured to always generate the builds when triggered
by a tag. In addition to releases, Apple Silicon tester builds for pull requests that might have special implications
for this target. For this reason, the workflow is configured to allow Apple Silicon builds to be triggered manually by a
repository maintainer.

The workflow uses a job matrix to run the build for each target on the appropriate runner machine in parallel, using the
universally applicable workflow code for all jobs. It uses another job matrix to generate individual workflow artifacts
for the tester builds of each target. Previously it was possible to always use the same matrix configurations for all
workflow runs. With the addition of the selectively run macOS ARM job, it is now necessary to generate these matrixes on
the fly.

The electron-updater package used by Arduino IDE's auto-update capability uses a data file (known as the "channel update
info file") to check for the availability of updates. A single "channel update info file" is used for the data of the
macOS x86 and ARM builds. Since a separate job is used to produce each of those builds, this means the "channel update
info file" produced by each of the macOS build jobs must be merged into a single file.
2023-10-06 09:50:52 -07:00
per1234
f0706e1849 Deduplicate type determination code in build workflow
The "Arduino IDE" GitHub Actions workflow is used to generate several distinct types of builds:

- Tester builds of commits
- Nightly builds
- Release builds

Different actions must be performed depending on which type of build is being produced. The workflow contains code that
uses various criteria to determine the build type.

Previously that code was duplicated in multiple places:

- The packaging job
- The changelog generation job
- The nightly build publishing job
- The release publishing job

This duplication is avoided by moving the code to a dedicated job that makes the build type information available to all
subsequent jobs via outputs.
2023-10-06 09:50:52 -07:00
per1234
4708bae9ab Use separate attributes for human identifier and runner identifier in build job matrix
The "Arduino IDE" GitHub Actions workflow uses a job matrix to make the builds for each host target in parallel.

The same steps are used for each job, but some configuration adjustments must be made on a per-target basis. This is
done through various attributes in the matrix configuration.

Previously the `os` attribute was used for two distinct things:

- The machine identifier of the GitHub Actions runner machine of the job.
- The differentiator in the human-targeted job name.

The attribute name "os" (for "operating system") was misleading because runners are differentiated by more than only the
operating system on the machine.

The use of a machine identifier as a differentiator in the human-targeted job name was a bad idea because these
identifiers would only effectively communicate the nature of a build to humans who are quite knowledgeable about the
GitHub Actions workflow syntax.

The impact of these poor decisions has not been too severe previously due to there only being a single job for each
operating system. However, there is a need for multiple jobs per operating system in order to support multiple host
architectures (e.g., macOS x86 and ARM).

The solution is to:

- Use an appropriate name for the runner identifier attribute.
- Use a dedicated attribute for the human friendly job name differentiator.
2023-10-06 09:50:52 -07:00
Akos Kitta
57975f8d91
fix: use board+port at startup if it's restored (#2242)
- update status bar if board+port is restored,
 - refresh the debug toolbar if board+port is restored,
 - init `Include Library` if board+port is ready, and
 - init library examples if board+port is ready

Closes #2237
Closes #2239

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-10-05 14:41:14 +02:00
Akos Kitta
8f4bcc83ec
chore(deps): update to theia@1.41.0 (#2211)
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-29 18:40:13 +02:00
Akos Kitta
ce02e263ec fix: storage service injection
Store the board config data per sketch and not per IDE installation.

The (Theia) default `StorageService` implementation is workspace-scoped
when `@theia/workspace` is part of the final application.

Closes #2240

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-27 17:34:24 +02:00
Akos Kitta
ed2d8ad13c chore(deps): update electron@25.5.0
- Update to `electron-builder@24.6.3`.
 - Fix obsolete electron security section in the development docs.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 10:06:47 +02:00
Akos Kitta
bb4b1450e3 chore: use Node.js >=18.17.0
remove `msvs_version` npm config on win32
Ref: nodejs/node-gyp#2822

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 10:06:47 +02:00
Akos Kitta
8a5dee9307 chore: format resources 💄
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 10:06:47 +02:00
Akos Kitta
5939b65511 chore: update prettier config
narrow the `resources` folder constraint in
the .gitignore

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 10:06:47 +02:00
Akos Kitta
7f660d76a8 fix: refresh the user-fields at app startup
Ref: arduino/arduino-ide#2165
Closes arduino/arduino-ide#2230

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 09:57:02 +02:00
Akos Kitta
9f48296d4d fix: defer board+port state update for extensions
If it is set before the board+port settings are restored from the
`localStorage`, extensions will see no board+port.

Ref: arduino/arduino-ide#2165
Ref: dankeboy36/esp-exception-decoder#10

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 09:56:34 +02:00
Akos Kitta
ec28623a97
fix: forward backend logging to electron (#2236)
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-26 09:45:03 +02:00
dependabot[bot]
73ddbefc3e build(deps): Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-05 00:27:27 -07:00
Akos Kitta
fe53b8e0d0 chore: update version to 2.2.2
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-09-01 08:53:21 +02:00
Akos Kitta
90b886ea92 chore: update to arduino-fwuploader@2.4.1
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-31 12:50:11 +02:00
Akos Kitta
97e26a9584 fix(ci): fix the changelog generation
Pinned `@octokit/rest` to `19.0.13`, so that it works with Node.js 16+.

Closes arduino/arduino-ide#2200

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-31 11:42:43 +02:00
Akos Kitta
f0704b678c fix(i18n): corrected the module for tr and ru
Closes arduino/arduino-ide#2201

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-31 08:48:05 +02:00
Akos Kitta
95bd2cf2e7 chore: update version to 2.2.1
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-29 17:12:17 +02:00
Akos Kitta
8d2808893c fix: name of translation (UA) module
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-29 08:21:11 +02:00
github-actions[bot]
e5b5b2a4be Updated translation files 2023-08-28 11:21:11 +02:00
Akos Kitta
e08439b6a4
fix: missing app icon for AppImage on Linux (#2190)
Closes #131

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-24 17:04:58 +02:00
Akos Kitta
e03e5eb603 chore: bump the version to 2.2.0
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-24 17:01:15 +02:00
Akos Kitta
b723951d78 chore(deps): Use arduino-cli@0.34.0
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-24 10:18:47 +02:00
Akos Kitta
ea2c54bff0 chore(deps): Use arduino-fwuploader@2.4.0
Ref: arduino/arduino-fwuploader#211
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-24 10:18:47 +02:00
Akos Kitta
5fd02b9fd7 fix: falsy context menu handlerId
The very first context menu item with ID `0` has not had a click handler

Ref: eclipse-theia/theia#12500
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-22 08:51:00 +02:00
per1234
10b38820bc Generalize name of "Firmware Updater"
Some Arduino boards have a supplemental hardware module that provides functionality separate from the primary
microcontroller the sketch program runs on.

Enhancements or fixes to the firmware that runs on these supplemental modules may be made over time so it is important
for the users of these boards to have an easy way to update the firmware. Arduino IDE provides a tool for doing this.

At the time the tool was created, the poor choice was made to include the names of the specific modules it supported at
the time in the tool's name. As was inevitable, that list has changed, rendering the tool name no longer accurate.

The immediate problem is that support has been added for updating the "bridge" and radio module on the UNO R4 WiFi. That
module is neither a "WiFi101" nor a "NINA", so the tool name does not reflect the support for the UNO R4 WiFi.

More significant changes in the supported modules are under way and will be introduced in the next release:

- Dropping support for the "WiFi101" module
- Adding support for the module on the Portenta C33 board

Rather than attempting to maintain a regularly changing and overly verbose name that includes the list of every
supported module, it is better to use a name for the tool that only describes its general purpose, leaving the task of
describing the specific supported modules to the documentation.
2023-08-21 08:44:10 -07:00
per1234
ea91904f00 Do full run of "Arduino IDE" workflow on tag push
The project was recently switched to using a "trunk-based" development strategy. This necessitated some adjustments to
the configuration of the GitHub Actions workflows in order to ensure the CI system could be used to effectively validate
the project at the state staged for release in the release branch.

A `run-determination` job was added to the workflow. This job determines whether the conditions under which the workflow
was triggered indicate that the rest of the jobs in the workflow should be run.

A validation workflow should run fully under any of the following conditions:

- The trigger event was something other than a branch creation
- The trigger event was a release branch creation

Since the project is fully validated prior to a release, running the workflow when triggered by a release tag is
pointless (and even harmful in some specific standardized workflows not currently used in this repository), so (even if
for no other reason than efficiency) verification workflows are configured to not run under these conditions.

The standardized Arduino tooling workflows follow a modular design where each workflow has a narrow scope of purpose.
That path was not taken by those who set up the infrastructure for this repository. They instead created a single
massive monolithic "Arduino IDE" workflow that performs many unrelated operations under various conditions. This workflow generates
releases in addition to doing validation. Even though it is pointless to run the workflow's validation operations when
the workflow is triggered by a release tag, it is essential to run it for the release generation.

Previously, the code used in the "Arduino IDE" workflow's `run-determination` job was configured as appropriate for a
verification workflow. This meant that a release would not be generated on push of a release tag as intended. The bug is
fixed by adjusting the code to do a full run of the workflow when triggered by a release tag.
2023-08-20 14:39:44 -07:00
Akos Kitta
57fa18b940 fix: do not start obsolete daemon watcher process
Before arduino/arduino-cli#488, IDE2 required a way to stop the daemon
process if the parent (backend) process crashed. However, this mechanism
is no longer necessary as the CLI daemon process is not actually a true
daemon process.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 21:54:13 +02:00
Akos Kitta
2aae9e0a07 fix: execute the Arduino CLI without a shell
Closes arduino/arduino-ide#2112
Ref: arduino/arduino-ide#2067

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 21:54:13 +02:00
Akos Kitta
9a99957e73
fix: incorrect certificate flashing command string (#2181)
Use a `string` array of command flags instead of the concatenated final
`string` command.

Ref: arduino/arduino-ide#2067
Closes arduino/arduino-ide#2179

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 21:53:43 +02:00
Akos Kitta
b25665561e fix: handle UNKNOWN code on syscall: 'stat'
Closes #2166

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 17:15:00 +02:00
Akos Kitta
420d31ff4b chore(cli): pin version to 0.34.0-rc.1
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 17:14:29 +02:00
Akos Kitta
db01efead3 fix: expand boards if available on detected port
moved the board inference logic from UI to  model

Closes #2175

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-20 17:14:29 +02:00
dependabot[bot]
5a76be306a build(deps): Bump svenstaro/upload-release-action from 2.6.1 to 2.7.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.6.1 to 2.7.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.6.1...2.7.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-18 20:53:13 -07:00
Akos Kitta
69ae38effa
feat: simplify board and port handling (#2165)
Use Arduino CLI revision `38479dc`

Closes #43
Closes #82
Closes #1319
Closes #1366
Closes #2143
Closes #2158

Ref: 38479dc706

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-18 14:42:50 +02:00
Akos Kitta
9a6a457bc4
chore(deps): Updated to Theia 1.39.0 (#2144)
- update Theia to `1.39.0`,
 - remove the application packager and fix the security vulnerabilities,
 - bundle the backed application with `webpack`, and
 - enhance the developer docs.

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
Co-authored-by: per1234 <accounts@perglass.com>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-14 12:12:05 +02:00
per1234
144df893d0 Clean localization data files before pulling from Transifex
The localization of the UI strings specific to the Arduino IDE application is done via the Transifex localization
platform. A scheduled workflow pulls the data from Transifex weekly and submits a pull request for the updates.

Previously, the script used to pull the data did not clean the data files before pulling. This meant that a vestigial
file would accumulate in the repository whenever a language was removed from the Transifex project.

The accumulation is avoided by deleting the data files for the locales managed on Transifex (all locales other than the
source English locale) before downloading the data from Transifex.
2023-07-26 01:10:25 -07:00
coby2023t
e17472ef07
Add 中文(繁體) ("Chinese (Traditional)") localization (#2151)
This will add a "中文(繁體)" option to the "Language" menu in the Arduino IDE preferences, which will cause the strings
in the IDE UI to be localized for "Chinese (Traditional)".

In addition to the translations for the strings that originate from the Eclipse Theia IDE framework provided by the
"Chinese (Traditional) Language Pack for Visual Studio Code" language pack, this will also utilize the translations of
the Arduino IDE-specific strings contributed by the community.
2023-07-25 23:52:04 -07:00
dankeboy36
f6a43254f5 feat: can dock the monitor widget to the "right"
Added a new preference (`arduino.monitor.dockPanel`) to specify the
location of the application shell where the _Serial Monitor_ widget
resides. The possible values are `"bottom"` and `"right"`. The default\
value is the `"bottom"`.

The dock panel is per application and not per workspace or window.
However, advanced users can create the `./.vscode/settings.json` and
configure per sketch preference.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2023-07-13 09:42:47 +02:00
Akos Kitta
94d2962985 fix: include all log args in the log message
- In the bundled application, the customized logger that writes the log
files bogusly includes only the first message fragment in the log
message. This PR ensures all message fragments are included in the final
message before it is printed to the console/terminal and persisted to
the rotating log files.
 - Includes messages with `debug` severity in the log.
 - Disables the ANSI coloring in the CLI daemon log by adding the
`NO_COLOR=true` environment variable to the spawned CLI daemon process.
 - Trims trailing line ending of the daemon log.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-07-07 17:28:16 +02:00
Akos Kitta
95fdc593ed chore(version): updated the version to 2.1.2
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-07-05 20:00:34 +02:00
dankeboy36
954fee41a0 fix(terminal): split-terminal visibility
Removed the toolbar contribution from the UI.

Ref: eclipse-theia/theia#12626/
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2023-07-05 16:58:44 +02:00
dankeboy36
0bcb182ec0 fix(terminal): widget flickering on resize
Ref: eclipse-theia/theia#12587
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2023-07-05 16:58:44 +02:00
dankeboy36
42d017e876 feat: expose Arduino state to VS Code extensions
- Update a shared state on fqbn, port, sketch path, and etc. changes.
VS Code extensions can access it and listen on changes.
 - Force VISX activation order: API VSIX starts first.

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2023-07-05 16:58:44 +02:00
Akos Kitta
c66b720509 chore(cli): Bumped the CLI version to 0.33.1
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-07-05 09:31:23 +02:00
Akos Kitta
a5c9735619 Revert "Revert "chore(cli): Bumped the CLI version to 0.33.0""
This reverts commit d0bce15f8baaa17b0272a5b8c0ba45e5577ed3a9.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-07-05 09:31:23 +02:00
per1234
6e18dca644 Adjust release procedure and CI system for "trunk-based" development strategy
Previously, releases were always made from a point in the revision history of the `main` branch (typically the tip of
the branch at the time of the release). Although the simplicity of this approach is nice, it can be limiting in some
cases. For this reason, the project is switching to using a "trunk-based" development strategy, as described here:

https://trunkbaseddevelopment.com/

This approach allows making releases at any time that consist of the arbitrary subset of revisions suitable for shipping
to the users at that time. The commits that should be included in the release are cherry-picked to a release branch and
the tag created on that branch.

This means that:

- PRs can be merged to the `main` branch as soon as they have passed review rather than having to postpone the merge of
  changes that are not ready to be included in the next release.
- Releases don't need to be postponed if the prior revision history on the `main` branch contains changes that are not
  ready to be included in the release.

The documented release procedure must be adjusted to reflect the new development strategy.

CI System Adjustments
---------------------

The status of the GitHub Actions workflows should be evaluated before making a release. However, this is not so simple as
checking the status of the commit at the tip of the release branch. The reason is that, for the sake of efficiency, the
workflows are configured to run only when the processes are relevant to the trigger event (e.g., no need to run unit
tests for a change to the readme).

In the case of the default branch, you can simply set the workflow runs filter to that branch and then check the result
of the latest run of each workflow of interest. However, that was not possible to do with the release branch since it
might be that the workflow was never run in that branch. The status of the latest run of the workflow in the default
branch might not match the status for the release branch if the release branch does not contain the full history.

For this reason, it will be helpful to trigger all relevant workflows on the creation of a release branch. This will
ensure that each of those workflows will always have at least one run in the release branch. Subsequent commits pushed to
the branch can run based on their usual trigger filters and the status of the latest run of each workflow in the branch
will provide an accurate indication of the state of that branch.

Branches are created for purposes other than releases, most notably feature branches to stage work for a pull request.
Due to the comprehensive nature of this project's CI system, it would not be convenient or efficient to fully run all CI
workflows on the creation of every feature branch.

Unfortunately, GitHub Actions does not support filters on the `create` event of branch creation like it does for the
`push` and `pull_request` events. There is support for a `branches` filter of the `push` event, but that filter is an
"AND" to the `paths` filter while this application requires an "OR". For this reason, the workflows must be triggered by
the creation of any branch. The unwanted job runs are prevented by adding a `run-determination` job with the branch
filter handled by Bash commands. The other jobs of the workflow use this `run-determination` job as a dependency, only
running when it indicates they should via a job output. Because this minimal `run-determination` job runs very quickly,
it is roughly equivalent to the workflow having been skipped entirely for non-release branch creations.
2023-06-30 11:31:47 -07:00
Akos Kitta
f3b99b08da 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:38:48 +02:00
Akos Kitta
d0bce15f8b Revert "chore(cli): Bumped the CLI version to 0.33.0"
This reverts commit 0fbd1fbe85fbbd864ad5e6a00850b32cbd2e83e0.
2023-06-30 14:02:08 +02:00
Andrew Sepulveda
d79bc0d083
add linux-arm64 to download-ls (#2078) 2023-06-12 13:45:58 +02:00
Akos Kitta
8f8b46fa6f fix: warn user when IDE cannot save the sketch
Happens when the IDE2 backend process crashes, and the communication
drops between the client and the server.

Closes #2081

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-09 12:11:31 +02:00
dependabot[bot]
d1fa6d4f3b build(deps): Bump svenstaro/upload-release-action from 2.5.0 to 2.6.1
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.5.0 to 2.6.1.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.5.0...2.6.1)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-09 00:02:33 -07:00
Akos Kitta
4af488b05e fix: relaxed saveAll if no Internet connection
The previous logic has incorrectly bailed the save when there is no
Internet connection. The corrected logic disallows saving files if there
is no connection between the frontend and the backend.

Ref: cff2c956845e04d320231e8a924d1a47ad016af7
Closes #2079

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-08 08:53:47 +02:00
Akos Kitta
db0049d635 fix: omit port from upload request when not set
Previously, if the `port` was not set in IDE2, the compile request
object has been created with an empty port object (`{}`). From now on,
if the port is not specified, IDE2 will create a compile request with
the default `null` `port` value.

Closes #2089

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-06 19:40:03 +02:00
Akos Kitta
0fbd1fbe85 chore(cli): Bumped the CLI version to 0.33.0
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-06 13:03:46 +02:00
Akos Kitta
9d2297c684 fix: removed unsafe shell when executing process
Ref: PNX-3671

Co-authored-by: per1234 <accounts@perglass.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-06-06 11:39:37 +02:00
Akos Kitta
e47fb2e651 fix: remove setting unsafe innerHTML
As it is vulnerable to stored Cross-Site Scripting.

Ref: PNX-3669
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-16 10:57:17 +02:00
Akos Kitta
ee43a12eb7 fix: show no errors if users cancel lib install
Throwing and catching a `UserAbortError` is part of the natural flow.

Closes #2063

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-16 10:56:39 +02:00
Akos Kitta
31deeebb49 test: use raw core index-update for the tests
It should make integration tests more resilient on the Windows CI.

From now on, tests are not starting a daemon to initialize the
`directories.data` folder for the test suites but rely on the raw
command. It is required to avoid spawning the discovery processes, which
cannot be terminated on Windows while the daemon is up and running.

Closes #2059

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-12 09:40:42 +02:00
Akos Kitta
117b2a4fc7 fix: do not enqueue write operations on conflict
Closes #2051

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-11 11:16:49 +02:00
Akos Kitta
278dd4ba87 fix: show notification if lib/core install failed
Closes #621

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-11 10:03:56 +02:00
Akos Kitta
36e2092398 build: use execa for the packager
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-09 17:37:24 +02:00
Akos Kitta
33ab2a6955 build: use execFileSync for npm scripts
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-09 17:37:24 +02:00
Akos Kitta
192aac5a81 chore: updated to Theia 1.37.0
- Updated `@theia/*` to `1.37.0`.
 - Fixed all `yarn audit` security vulnerabilities.
 - Updated to `electron@23.2.4`:
   - `contextIsolation` is `true`,
   - `nodeIntegration` is `false`, and the
   - `webpack` target is moved from `electron-renderer` to `web`.
 - Updated to `typescript@4.9.3`.
 - Updated the `eslint` plugins.
 - Added the new `Light High Contrast` theme to the IDE2.
 - High contrast themes use Theia APIs for style adjustments.
 - Support for ESM modules: `"moduleResolution": "node16"`.
 - Node.js >= 16.14 is required.
 - VISX langage packs were bumped to `1.70.0`.
 - Removed undesired editor context menu items. (Closes #1394)

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-09 17:37:24 +02:00
Akos Kitta
964ea3bc0c fix: copy when punctuation marks in sketch path
Changed the `source` and `cwd` args to avoid accidentally creating an
invalid `glob` patterns when doing the brace expansion by `cpy`.

Closes #2043

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-06 14:32:13 +02:00
Akos Kitta
e6828f86d7 fix: do not exclude cloud sketch diagnostics
PROEDITOR-50: error markers have been disabled for built-ins (daedae1).
Relaxed the error marker filtering for cloud sketches.

Closes #669

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-05 14:15:42 +02:00
Akos Kitta
51f69f6a59 test: gRPC core client init integration test
- Copied the env-variable server from Theia and made it possible to
customize it for the tests. Each test has its own `data` folder.
 - Relaxed the primary package and library index error detection.
This should make the init error detection locale independent.
 - Kill the daemon process subtree when stopping the daemon.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-05-03 15:38:17 +02:00
Akos Kitta
097c92d904 fix: reduced unnecessary GET /sketches request
Ref: #1849

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-24 09:47:13 +02:00
Muhammad Zaheer
b451e2db8c Corrected library.properties assignment w.r.t description and summary
Ref: arduino/arduino-ide#1611
2023-04-21 14:43:14 +02:00
per1234
b3b94948a3 Document 3rd party theme installation in "Advanced Usage"
Arduino IDE comes with a selection of officially maintained and supported built-in themes:

- Light
- Dark
- High Contrast

Although these three should be sufficient for most users, some users may have other requirements or preferences for the
Arduino IDE UI theming. Fortunately, because it is built on the Eclipse Theia IDE framework, Arduino IDE supports VS
Code theme extensions. This makes a large variety of 3rd party themes available to the user, and even the ability to
create custom themes.

Instructions for installing VS Code theme extensions are here added to the "Advanced Usage" document hosted in the
repository.
2023-04-20 11:11:24 -07:00
Akos Kitta
80afd382ad chore: Updated to the next version after release.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-04-20 17:15:55 +02:00
494 changed files with 53309 additions and 37179 deletions

View File

@ -1,63 +1,66 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
ignorePatterns: [
'node_modules/*',
'**/node_modules/*',
'.node_modules/*',
'.github/*',
'.browser_modules/*',
'docs/*',
'scripts/*',
'electron-app/*',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
},
ignorePatterns: [
'node_modules/*',
'**/node_modules/*',
'.github/*',
'.browser_modules/*',
'docs/*',
'scripts/*',
'electron-app/lib/*',
'electron-app/src-gen/*',
'electron-app/gen-webpack*.js',
'!electron-app/webpack.config.js',
'electron-app/plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
'**/lib/*',
],
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:react-hooks/recommended', // Uses recommended rules from react hooks
'plugin:prettier/recommended',
'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
],
plugins: ['prettier', 'unused-imports'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-empty-interface': 'warn',
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:react-hooks/recommended', // Uses recommended rules from react hooks
'plugin:prettier/recommended',
'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
],
plugins: ['prettier', 'unused-imports'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-empty-interface': 'warn',
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
'react/display-name': 'warn',
eqeqeq: ['error', 'smart'],
'guard-for-in': 'off',
'id-blacklist': 'off',
'id-match': 'off',
'no-underscore-dangle': 'off',
'no-unused-expressions': 'off',
'no-var': 'error',
radix: 'error',
'prettier/prettier': 'warn',
},
'react/display-name': 'warn',
eqeqeq: ['error', 'smart'],
'guard-for-in': 'off',
'id-blacklist': 'off',
'id-match': 'off',
'no-underscore-dangle': 'off',
'no-unused-expressions': 'off',
'no-var': 'error',
radix: 'error',
'prettier/prettier': 'warn',
},
};

View File

@ -1,7 +1,7 @@
name: Bug report
description: Report a problem with the code or documentation in this repository.
labels:
- "type: imperfection"
- 'type: imperfection'
body:
- type: textarea
id: description

View File

@ -1,7 +1,7 @@
name: Feature request
description: Suggest an enhancement to this project.
labels:
- "type: enhancement"
- 'type: enhancement'
body:
- type: textarea
id: description

View File

@ -1,15 +1,18 @@
### Motivation
<!-- Why this pull request? -->
### Change description
<!-- What does your code do? -->
### Other information
<!-- Any additional information that could help the review process -->
### Reviewer checklist
* [ ] PR addresses a single concern.
* [ ] The PR has no duplicates (please search among the [Pull Requests](https://github.com/arduino/arduino-ide/pulls) before creating one)
* [ ] PR title and description are properly filled.
* [ ] Docs have been added / updated (for bug fixes / features)
- [ ] PR addresses a single concern.
- [ ] The PR has no duplicates (please search among the [Pull Requests](https://github.com/arduino/arduino-ide/pulls) before creating one)
- [ ] PR title and description are properly filled.
- [ ] Docs have been added / updated (for bug fixes / features)

View File

@ -12,4 +12,4 @@ updates:
schedule:
interval: daily
labels:
- "topic: infrastructure"
- 'topic: infrastructure'

View File

@ -1,27 +1,27 @@
# Used by the "Sync Labels" workflow
# See: https://github.com/Financial-Times/github-label-sync#label-config-file
- name: "topic: accessibility"
color: "00ffff"
- name: 'topic: accessibility'
color: '00ffff'
description: Enabling the use of the software by everyone
- name: "topic: CLI"
color: "00ffff"
- name: 'topic: CLI'
color: '00ffff'
description: Related to Arduino CLI
- name: "topic: cloud"
color: "00ffff"
- name: 'topic: cloud'
color: '00ffff'
description: Related to Arduino Cloud and cloud sketches
- name: "topic: debugger"
color: "00ffff"
- name: 'topic: debugger'
color: '00ffff'
description: Related to the integrated debugger
- name: "topic: language server"
color: "00ffff"
- name: 'topic: language server'
color: '00ffff'
description: Related to the Arduino Language Server
- name: "topic: serial monitor"
color: "00ffff"
- name: 'topic: serial monitor'
color: '00ffff'
description: Related to the Serial Monitor
- name: "topic: theia"
color: "00ffff"
- name: 'topic: theia'
color: '00ffff'
description: Related to the Theia IDE framework
- name: "topic: theme"
color: "00ffff"
- name: 'topic: theme'
color: '00ffff'
description: Related to GUI theming

View File

@ -0,0 +1,63 @@
# The Arduino IDE Linux build workflow job runs in this container.
# syntax=docker/dockerfile:1
# See: https://hub.docker.com/_/ubuntu/tags
FROM ubuntu:18.10
# This is required in order to use the Ubuntu package repositories for EOL Ubuntu versions:
# https://help.ubuntu.com/community/EOLUpgrades#Update_sources.list
RUN \
sed \
--in-place \
--regexp-extended \
--expression='s/([a-z]{2}\.)?archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' \
"/etc/apt/sources.list"
RUN \
apt-get \
--yes \
update
RUN \
apt-get \
--yes \
install \
"git"
# The repository path must be added to safe.directory, otherwise any Git operations on it would fail with a
# "dubious ownership" error. actions/checkout configures this, but it is not applied to containers.
RUN \
git config \
--add \
--global \
"safe.directory" "/__w/arduino-ide/arduino-ide"
ENV \
GIT_CONFIG_GLOBAL="/root/.gitconfig"
# Install Python
# The Python installed by actions/setup-python has dependency on a higher version of glibc than available in the
# container.
RUN \
apt-get \
--yes \
install \
"python3.7-minimal=3.7.3-2~18.10"
# Install Theia's package dependencies
# These are pre-installed in the GitHub Actions hosted runner machines.
RUN \
apt-get \
--yes \
install \
"libsecret-1-dev=0.18.6-3" \
"libx11-dev=2:1.6.7-1" \
"libxkbfile-dev=1:1.0.9-2"
# Target python3 symlink to Python 3.7 installation. It would otherwise target version 3.6 due to the installation of
# the `python3` package as a transitive dependency.
RUN \
ln \
--symbolic \
--force \
"$(which python3.7)" \
"/usr/bin/python3"

View File

@ -1,20 +1,28 @@
name: Arduino IDE
on:
create:
push:
branches:
- main
- '[0-9]+.[0-9]+.x'
paths-ignore:
- '.github/**'
- '!.github/workflows/build.yml'
- '.vscode/**'
- 'docs/**'
- 'scripts/**'
- '!scripts/merge-channel-files.js'
- 'static/**'
- '*.md'
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
inputs:
paid-runners:
description: Include builds on non-free runners
type: boolean
default: false
pull_request:
paths-ignore:
- '.github/**'
@ -22,78 +30,346 @@ on:
- '.vscode/**'
- 'docs/**'
- 'scripts/**'
- '!scripts/merge-channel-files.js'
- 'static/**'
- '*.md'
schedule:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
workflow_run:
workflows:
- Push Container Images
branches:
- main
types:
- completed
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
JOB_TRANSFER_ARTIFACT: build-artifacts
GO_VERSION: '1.21'
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: '18.17'
YARN_VERSION: '1.22'
JOB_TRANSFER_ARTIFACT_PREFIX: build-artifacts-
CHANGELOG_ARTIFACTS: changelog
STAGED_CHANNEL_FILE_ARTIFACT_PREFIX: staged-channel-file-
BASE_BUILD_DATA: |
- config:
# Human identifier for the job.
name: Windows
runs-on: [self-hosted, windows-sign-pc]
# The value is a string representing a JSON document.
# Setting this to null causes the job to run directly in the runner machine instead of in a container.
container: |
null
# Name of the secret that contains the certificate.
certificate-secret: INSTALLER_CERT_WINDOWS_CER
# Name of the secret that contains the certificate password.
certificate-password-secret: INSTALLER_CERT_WINDOWS_PASSWORD
# File extension for the certificate.
certificate-extension: pfx
# Container for windows cert signing
certificate-container: INSTALLER_CERT_WINDOWS_CONTAINER
# Arbitrary identifier used to give the workflow artifact uploaded by each "build" matrix job a unique name.
job-transfer-artifact-suffix: Windows_64bit
# Quoting on the value is required here to allow the same comparison expression syntax to be used for this
# and the companion needs.select-targets.outputs.merge-channel-files property (output values always have string
# type).
mergeable-channel-file: 'false'
# as this runs on a self hosted runner, we need to avoid building with the default working directory path,
# otherwise paths in the build job will be too long for `light.exe`
# we use the below as a Symbolic link (just changing the wd will break the checkout action)
# this is a work around (see: https://github.com/actions/checkout/issues/197).
working-directory: 'C:\a'
artifacts:
- path: '*Windows_64bit.exe'
name: Windows_X86-64_interactive_installer
- path: '*Windows_64bit.msi'
name: Windows_X86-64_MSI
- path: '*Windows_64bit.zip'
name: Windows_X86-64_zip
- config:
name: Linux
runs-on: ubuntu-latest
container: |
{
\"image\": \"ghcr.io/arduino/arduino-ide/linux:main\"
}
job-transfer-artifact-suffix: Linux_64bit
mergeable-channel-file: 'false'
artifacts:
- path: '*Linux_64bit.zip'
name: Linux_X86-64_zip
- path: '*Linux_64bit.AppImage'
name: Linux_X86-64_app_image
- config:
name: macOS x86
runs-on: macos-13
container: |
null
# 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
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
job-transfer-artifact-suffix: macOS_64bit
mergeable-channel-file: 'true'
artifacts:
- path: '*macOS_64bit.dmg'
name: macOS_X86-64_dmg
- path: '*macOS_64bit.zip'
name: macOS_X86-64_zip
- config:
name: macOS ARM
runs-on: macos-latest
container: |
null
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
job-transfer-artifact-suffix: macOS_arm64
mergeable-channel-file: 'true'
artifacts:
- path: '*macOS_arm64.dmg'
name: macOS_arm64_dmg
- path: '*macOS_arm64.zip'
name: macOS_arm64_zip
PAID_RUNNER_BUILD_DATA: |
# This system was implemented to allow selective use of paid GitHub-hosted runners, due to the Apple Silicon runner
# incurring a charge at that time. Free Apple Silicon runners are now available so the configuration was moved to
# `BASE_BUILD_DATA`, but the system was left in place for future use.
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
permissions: {}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
build-type-determination:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
outputs:
is-release: ${{ steps.determination.outputs.is-release }}
is-nightly: ${{ steps.determination.outputs.is-nightly }}
channel-name: ${{ steps.determination.outputs.channel-name }}
publish-to-s3: ${{ steps.determination.outputs.publish-to-s3 }}
environment: production
permissions: {}
steps:
- name: Determine the type of build
id: determination
run: |
if [[
"${{ startsWith(github.ref, 'refs/tags/') }}" == "true"
]]; then
is_release="true"
is_nightly="false"
channel_name="stable"
elif [[
"${{ github.event_name }}" == "schedule" ||
(
"${{ github.event_name }}" == "workflow_dispatch" &&
"${{ github.ref }}" == "refs/heads/main"
)
]]; then
is_release="false"
is_nightly="true"
channel_name="nightly"
else
is_release="false"
is_nightly="false"
channel_name="nightly"
fi
echo "is-release=$is_release" >> $GITHUB_OUTPUT
echo "is-nightly=$is_nightly" >> $GITHUB_OUTPUT
echo "channel-name=$channel_name" >> $GITHUB_OUTPUT
# Only attempt upload to Amazon S3 if the credentials are available.
echo "publish-to-s3=${{ secrets.AWS_ROLE_ARN != '' }}" >> $GITHUB_OUTPUT
select-targets:
needs: build-type-determination
runs-on: ubuntu-latest
outputs:
artifact-matrix: ${{ steps.assemble.outputs.artifact-matrix }}
build-matrix: ${{ steps.assemble.outputs.build-matrix }}
merge-channel-files: ${{ steps.assemble.outputs.merge-channel-files }}
permissions: {}
steps:
- name: Assemble target data
id: assemble
run: |
# Only run the builds that incur runner charges on release or select manually triggered runs.
if [[
"${{ needs.build-type-determination.outputs.is-release }}" == "true" ||
"${{ github.event.inputs.paid-runners }}" == "true"
]]; then
build_matrix="$(
(
echo "${{ env.BASE_BUILD_DATA }}";
echo "${{ env.PAID_RUNNER_BUILD_DATA }}"
) | \
yq \
--output-format json \
'[.[].config]'
)"
artifact_matrix="$(
(
echo "${{ env.BASE_BUILD_DATA }}";
echo "${{ env.PAID_RUNNER_BUILD_DATA }}"
) | \
yq \
--output-format json \
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))'
)"
# The build matrix produces two macOS jobs (x86 and ARM) so the "channel update info files"
# generated by each must be merged.
merge_channel_files="true"
else
build_matrix="$(
echo "${{ env.BASE_BUILD_DATA }}" | \
yq \
--output-format json \
'[.[].config]'
)"
artifact_matrix="$(
echo "${{ env.BASE_BUILD_DATA }}" | \
yq \
--output-format json \
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))'
)"
merge_channel_files="false"
fi
# Set workflow step outputs.
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
delimiter="$RANDOM"
echo "build-matrix<<$delimiter" >> $GITHUB_OUTPUT
echo "$build_matrix" >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
delimiter="$RANDOM"
echo "artifact-matrix<<$delimiter" >> $GITHUB_OUTPUT
echo "$artifact_matrix" >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
echo "merge-channel-files=$merge_channel_files" >> $GITHUB_OUTPUT
build:
name: build (${{ matrix.config.os }})
name: build (${{ matrix.config.name }})
needs:
- build-type-determination
- select-targets
env:
# Location of artifacts generated by build.
BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts
# to skip passing signing credentials to electron-builder
IS_WINDOWS_CONFIG: ${{ matrix.config.name == 'Windows' }}
INSTALLER_CERT_WINDOWS_CER: "/tmp/cert.cer"
# We are hardcoding the path for signtool because is not present on the windows PATH env var by default.
# Keep in mind that this path could change when upgrading to a new runner version
SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x86/signtool.exe"
WIN_CERT_PASSWORD: ${{ secrets[matrix.config.certificate-password-secret] }}
WIN_CERT_CONTAINER_NAME: ${{ secrets[matrix.config.certificate-container] }}
PUPPETEER_SKIP_DOWNLOAD: true
strategy:
matrix:
config:
- os: windows-2019
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-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
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
runs-on: ${{ matrix.config.os }}
config: ${{ fromJson(needs.select-targets.outputs.build-matrix) }}
runs-on: ${{ matrix.config.runs-on }}
container: ${{ fromJSON(matrix.config.container) }}
defaults:
run:
# Avoid problems caused by different default shell for container jobs (sh) vs non-container jobs (bash).
shell: bash
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Symlink custom working directory
shell: cmd
if: runner.os == 'Windows' && matrix.config.working-directory
run: |
if not exist "${{ matrix.config.working-directory }}" mklink /d "${{ matrix.config.working-directory }}" "C:\actions-runner\_work\arduino-ide\arduino-ide"
- name: Install Node.js 16.x
uses: actions/setup-node@v3
- name: Checkout
uses: actions/checkout@v4
- name: Install Node.js
if: runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
# Yarn is a prerequisite for the action's cache feature, so caching should be disabled when running in the
# container where Yarn is not pre-installed.
cache: ${{ fromJSON(matrix.config.container) == null && 'yarn' || null }}
- name: Install Yarn
if: runner.name != 'WINDOWS-SIGN-PC'
run: |
npm \
install \
--global \
"yarn@${{ env.YARN_VERSION }}"
- name: Install Python 3.x
uses: actions/setup-python@v4
if: fromJSON(matrix.config.container) == null && runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: '3.11.x'
- name: Install Go
uses: actions/setup-go@v4
if: runner.name != 'WINDOWS-SIGN-PC'
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
if: runner.name != 'WINDOWS-SIGN-PC'
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Package
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }}
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
working-directory: ${{ matrix.config.working-directory || './' }}
run: |
# See: https://www.electron.build/code-signing
if [ $CAN_SIGN = false ]; then
if [ $CAN_SIGN = false ] || [ $IS_WINDOWS_CONFIG = true ]; then
echo "Skipping the app signing: certificate not provided."
else
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
@ -102,71 +378,169 @@ jobs:
export CSC_FOR_PULL_REQUEST=true
fi
if [ "${{ runner.OS }}" = "Windows" ]; then
npm config set msvs_version 2017 --global
fi
npx node-gyp install
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
yarn install
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v3
yarn --cwd arduino-ide-extension build
yarn --cwd electron-app rebuild
yarn --cwd electron-app build
yarn --cwd electron-app package
# Both macOS jobs generate a "channel update info file" with same path and name. The second job to complete would
# overwrite the file generated by the first in the workflow artifact.
- name: Stage channel file for merge
if: >
needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true'
working-directory: ${{ matrix.config.working-directory || './' }}
run: |
staged_channel_files_path="${{ runner.temp }}/staged-channel-files"
mkdir "$staged_channel_files_path"
mv \
"${{ env.BUILD_ARTIFACTS_PATH }}/${{ needs.build-type-determination.outputs.channel-name }}-mac.yml" \
"${staged_channel_files_path}/${{ needs.build-type-determination.outputs.channel-name }}-mac-${{ runner.arch }}.yml"
# Set workflow environment variable for use in other steps.
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable
echo "STAGED_CHANNEL_FILES_PATH=$staged_channel_files_path" >> "$GITHUB_ENV"
- name: Upload staged-for-merge channel file artifact
uses: actions/upload-artifact@v4
if: >
needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true'
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: electron/build/dist/build-artifacts/
if-no-files-found: error
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.STAGED_CHANNEL_FILES_PATH) || env.STAGED_CHANNEL_FILES_PATH }}
- name: Upload builds to job transfer artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.BUILD_ARTIFACTS_PATH) || env.BUILD_ARTIFACTS_PATH }}
- name: Manual Clean up for self-hosted runners
if: runner.os == 'Windows' && matrix.config.working-directory
shell: cmd
run: |
rmdir /s /q "${{ matrix.config.working-directory }}\${{ env.BUILD_ARTIFACTS_PATH }}"
merge-channel-files:
needs:
- build-type-determination
- select-targets
- build
if: needs.select-targets.outputs.merge-channel-files == 'true'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Set environment variables
run: |
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable
echo "CHANNEL_FILES_PATH=${{ runner.temp }}/channel-files" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
- name: Download staged-for-merge channel file artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: ${{ env.CHANNEL_FILES_PATH }}
pattern: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}*
- name: Remove no longer needed artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}*
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
- name: Merge "channel update info files"
run: |
node \
./scripts/merge-channel-files.js \
--channel "${{ needs.build-type-determination.outputs.channel-name }}" \
--input "${{ env.CHANNEL_FILES_PATH }}"
- name: Upload merged channel files job transfer artifact
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}channel-files
path: ${{ env.CHANNEL_FILES_PATH }}
artifacts:
name: ${{ matrix.artifact.name }} artifact
needs: build
needs:
- select-targets
- build
if: always() && needs.build.result != 'skipped'
runs-on: ubuntu-latest
env:
BUILD_ARTIFACTS_FOLDER: build-artifacts
strategy:
matrix:
artifact:
- path: '*Linux_64bit.zip'
name: Linux_X86-64_zip
- path: '*Linux_64bit.AppImage'
name: Linux_X86-64_app_image
- path: '*macOS_64bit.dmg'
name: macOS_dmg
- path: '*macOS_64bit.zip'
name: macOS_zip
- path: '*Windows_64bit.exe'
name: Windows_X86-64_interactive_installer
- path: '*Windows_64bit.msi'
name: Windows_X86-64_MSI
- path: '*Windows_64bit.zip'
name: Windows_X86-64_zip
artifact: ${{ fromJson(needs.select-targets.outputs.artifact-matrix) }}
steps:
- name: Download job transfer artifact
uses: actions/download-artifact@v3
- name: Download job transfer artifact that contains ${{ matrix.artifact.name }} tester build
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.artifact.job-transfer-artifact-suffix }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }}
- name: Upload tester build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact.name }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }}/${{ matrix.artifact.path }}
changelog:
needs: build
needs:
- build-type-determination
- build
runs-on: ubuntu-latest
outputs:
BODY: ${{ steps.changelog.outputs.BODY }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0 # To fetch all history for all branches and tags.
- name: Generate Changelog
id: changelog
env:
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
run: |
export LATEST_TAG=$(git describe --abbrev=0)
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
@ -191,44 +565,89 @@ jobs:
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v3
- name: Upload changelog job transfer artifact
if: needs.build-type-determination.outputs.is-nightly == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}changelog
path: CHANGELOG.txt
publish:
needs: changelog
if: github.repository == 'arduino/arduino-ide' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
needs:
- build-type-determination
- merge-channel-files
- changelog
if: >
always() &&
needs.build-type-determination.result == 'success' &&
(
needs.merge-channel-files.result == 'skipped' ||
needs.merge-channel-files.result == 'success'
) &&
needs.changelog.result == 'success' &&
needs.build-type-determination.outputs.publish-to-s3 == 'true' &&
needs.build-type-determination.outputs.is-nightly == 'true'
runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: read
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v3
- name: Download all job transfer artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
merge-multiple: true
path: ${{ env.ARTIFACTS_FOLDER }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Configure AWS Credentials for Nightly [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Nightly [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide/nightly'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/nightly
release:
needs: changelog
if: startsWith(github.ref, 'refs/tags/')
needs:
- build-type-determination
- merge-channel-files
- changelog
if: >
always() &&
needs.build-type-determination.result == 'success' &&
(
needs.merge-channel-files.result == 'skipped' ||
needs.merge-channel-files.result == 'success'
) &&
needs.changelog.result == 'success' &&
needs.build-type-determination.outputs.is-release == 'true'
runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: write
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v3
- name: Download all job transfer artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
merge-multiple: true
path: ${{ env.ARTIFACTS_FOLDER }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Get Tag
id: tag_name
@ -236,39 +655,32 @@ jobs:
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.5.0
uses: svenstaro/upload-release-action@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
file: ${{ env.JOB_TRANSFER_ARTIFACT }}/*
file: ${{ env.ARTIFACTS_FOLDER }}/*
tag: ${{ github.ref }}
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: Configure AWS Credentials for Release [S3]
if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Release [S3]
if: github.repository == 'arduino/arduino-ide'
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
run: |
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide
clean:
# This job must run after all jobs that use the transfer artifact.
needs:
- build
- merge-channel-files
- publish
- release
- artifacts
@ -276,7 +688,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v2
- name: Remove unneeded job transfer artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*

View File

@ -3,6 +3,7 @@ name: Check Certificates
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-certificates.ya?ml'
@ -20,12 +21,49 @@ env:
EXPIRATION_WARNING_PERIOD: 30
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
REPO_SLUG="arduino/arduino-ide"
if [[
(
# Only run on branch creation when it is a release branch.
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
) &&
(
# Only run when the workflow will have access to the certificate secrets.
# This could be done via a GitHub Actions workflow conditional, but makes more sense to do it here as well.
(
"${{ github.event_name }}" != "pull_request" &&
"${{ github.repository }}" == "$REPO_SLUG"
) ||
(
"${{ github.event_name }}" == "pull_request" &&
"${{ github.event.pull_request.head.repo.full_name }}" == "$REPO_SLUG"
)
)
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check-certificates:
name: ${{ matrix.certificate.identifier }}
# Only run when the workflow will have access to the certificate secrets.
if: >
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@ -36,9 +74,11 @@ jobs:
- identifier: macOS signing certificate # Text used to identify certificate in notifications.
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
type: pkcs12
- identifier: Windows signing certificate
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
certificate-secret: INSTALLER_CERT_WINDOWS_CER
# The password for the Windows certificate is not needed, because its not a container, but a single certificate.
type: x509
steps:
- name: Set certificate path environment variable
@ -57,7 +97,7 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
run: |
(
openssl pkcs12 \
openssl ${{ matrix.certificate.type }} \
-in "${{ env.CERTIFICATE_PATH }}" \
-legacy \
-noout \
@ -84,26 +124,43 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
id: get-days-before-expiration
run: |
EXPIRATION_DATE="$(
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
openssl x509 \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
if [[ ${{ matrix.certificate.type }} == "pkcs12" ]]; then
EXPIRATION_DATE="$(
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
openssl x509 \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
elif [[ ${{ matrix.certificate.type }} == "x509" ]]; then
EXPIRATION_DATE="$(
(
openssl x509 \
-in ${{ env.CERTIFICATE_PATH }} \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
fi
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"

58
.github/workflows/check-containers.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Check Containers
on:
pull_request:
paths:
- ".github/workflows/check-containers.ya?ml"
- "**.Dockerfile"
- "**/Dockerfile"
push:
paths:
- ".github/workflows/check-containers.ya?ml"
- "**.Dockerfile"
- "**/Dockerfile"
repository_dispatch:
schedule:
# Run periodically to catch breakage caused by external changes.
- cron: "0 7 * * MON"
workflow_dispatch:
jobs:
run:
name: Run (${{ matrix.image.path }})
runs-on: ubuntu-latest
permissions: {}
services:
registry:
image: registry:2
ports:
- 5000:5000
env:
IMAGE_NAME: name/app:latest
REGISTRY: localhost:5000
strategy:
fail-fast: false
matrix:
image:
- path: .github/workflows/assets/linux.Dockerfile
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and push to local registry
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.path }}
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Run container
run: |
docker \
run \
--rm \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

View File

@ -2,10 +2,11 @@ name: Check Internationalization
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: '1.21'
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-i18n-task.ya?ml'
@ -22,32 +23,67 @@ on:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
permissions: {}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
TAG_REGEX="refs/tags/.*"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
("${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX) &&
! "${{ github.ref }}" =~ $TAG_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js 16.x
uses: actions/setup-node@v3
- name: Install Node.js 18.17
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '18.17'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
run: yarn install --immutable
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

94
.github/workflows/check-javascript.yml vendored Normal file
View File

@ -0,0 +1,94 @@
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-javascript-task.md
name: Check JavaScript
env:
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
# See: https://docs.github.com/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
pull_request:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Lint
run: |
yarn \
--cwd arduino-ide-extension \
lint

97
.github/workflows/check-yarn.yml vendored Normal file
View File

@ -0,0 +1,97 @@
name: Check Yarn
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
pull_request:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
schedule:
# Run every Tuesday at 8 AM UTC to catch breakage resulting from changes to the JSON schema.
- cron: "0 8 * * TUE"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check-sync:
name: check-sync (${{ matrix.project.path }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn \
install \
--ignore-scripts
- name: Check yarn.lock
run: |
git \
diff \
--color \
--exit-code \
"${{ matrix.project.path }}/yarn.lock"

View File

@ -8,22 +8,33 @@ on:
env:
CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 16.x
NODE_VERSION: '18.17'
jobs:
create-changelog:
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Get Tag
id: tag_name
run: |
@ -32,7 +43,7 @@ jobs:
- name: Create full changelog
id: full-changelog
run: |
yarn add @octokit/rest --ignore-workspace-root-check
yarn add @octokit/rest@19.0.13 --ignore-workspace-root-check
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
# Get the changelog file name to build
@ -44,12 +55,12 @@ jobs:
# Compose changelog
yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME"
- name: Configure AWS Credentials for Changelog [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Changelog [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/'
PLUGIN_TARGET: '/arduino-ide/changelog'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ${{ env.CHANGELOG_ARTIFACTS }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/changelog

View File

@ -2,7 +2,7 @@ name: i18n-nightly-push
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: '1.21'
on:
schedule:
@ -14,27 +14,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js 16.x
uses: actions/setup-node@v3
- name: Install Node.js 18.17
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '18.17'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
run: yarn install --immutable
- name: Run i18n:push script
run: yarn run i18n:push

View File

@ -2,7 +2,7 @@ name: i18n-weekly-pull
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: '1.21'
on:
schedule:
@ -14,27 +14,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js 16.x
uses: actions/setup-node@v3
- name: Install Node.js 18.17
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '18.17'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
run: yarn install --immutable
- name: Run i18n:pull script
run: yarn run i18n:pull
@ -45,7 +52,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v7
with:
commit-message: Updated translation files
title: Update translation files

View File

@ -0,0 +1,70 @@
name: Push Container Images
on:
pull_request:
paths:
- ".github/workflows/push-container-images.ya?ml"
push:
paths:
- ".github/workflows/push-container-images.ya?ml"
- "**.Dockerfile"
- "**/Dockerfile"
repository_dispatch:
schedule:
# Run periodically to catch breakage caused by external changes.
- cron: "0 8 * * MON"
workflow_dispatch:
jobs:
push:
name: Push (${{ matrix.image.name }})
# Only run the job when GITHUB_TOKEN has the privileges required for Container registry login.
if: >
(
github.event_name != 'pull_request' &&
github.repository == 'arduino/arduino-ide'
) ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide'
)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
image:
- path: .github/workflows/assets/linux.Dockerfile
name: ${{ github.repository }}/linux
registry: ghcr.io
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
password: ${{ secrets.GITHUB_TOKEN }}
registry: ${{ matrix.image.registry }}
username: ${{ github.repository_owner }}
- name: Extract metadata for image
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image.registry }}/${{ matrix.image.name }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.path }}
labels: ${{ steps.metadata.outputs.labels }}
# Workflow is triggered on relevant events for the sake of a "dry run" validation but image is only pushed to
# registry on commit to the main branch.
push: ${{ github.ref == 'refs/heads/main' }}
tags: ${{ steps.metadata.outputs.tags }}

View File

@ -5,21 +5,21 @@ name: Sync Labels
on:
push:
paths:
- ".github/workflows/sync-labels.ya?ml"
- ".github/label-configuration-files/*.ya?ml"
- '.github/workflows/sync-labels.ya?ml'
- '.github/label-configuration-files/*.ya?ml'
pull_request:
paths:
- ".github/workflows/sync-labels.ya?ml"
- ".github/label-configuration-files/*.ya?ml"
- '.github/workflows/sync-labels.ya?ml'
- '.github/label-configuration-files/*.ya?ml'
schedule:
# Run daily at 8 AM UTC to sync with changes to shared label configurations.
- cron: "0 8 * * *"
- cron: '0 8 * * *'
workflow_dispatch:
repository_dispatch:
env:
CONFIGURATIONS_FOLDER: .github/label-configuration-files
CONFIGURATIONS_ARTIFACT: label-configuration-files
CONFIGURATIONS_ARTIFACT_PREFIX: label-configuration-file-
jobs:
check:
@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download JSON schema for labels configuration file
id: download-schema
@ -71,13 +71,13 @@ jobs:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
- name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
path: |
*.yaml
*.yml
if-no-files-found: error
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}${{ matrix.filename }}
sync:
needs: download
@ -106,18 +106,19 @@ jobs:
echo "flag=--dry-run" >> $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download configuration files artifact
uses: actions/download-artifact@v3
- name: Download configuration file artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
merge-multiple: true
pattern: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}*
path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v2
- name: Remove unneeded artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}*
- name: Merge label configuration files
run: |

140
.github/workflows/test-javascript.yml vendored Normal file
View File

@ -0,0 +1,140 @@
name: Test JavaScript
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21'
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
on:
push:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
pull_request:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
test:
name: test (${{ matrix.project.path }}, ${{ matrix.operating-system }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ${{ matrix.operating-system }}
defaults:
run:
shell: bash
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
operating-system:
- macos-latest
- ubuntu-latest
- windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
# See: https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.11.x'
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Compile TypeScript
run: |
yarn \
--cwd arduino-ide-extension \
build
- name: Run tests
env:
# These secrets are optional. Dependent tests will be skipped if not available.
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
run: |
yarn test
yarn \
--cwd arduino-ide-extension \
test:slow

View File

@ -8,35 +8,42 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
NODE_VERSION: 16.x
GO_VERSION: '1.21'
NODE_VERSION: '18.17'
jobs:
pull-from-jsonbin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies
run: yarn
run: yarn install --immutable
- name: Run themes:pull script
run: yarn run themes:pull
@ -54,7 +61,7 @@ jobs:
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v7
with:
commit-message: Updated themes
title: Update themes

13
.gitignore vendored
View File

@ -1,25 +1,22 @@
node_modules/
# .node_modules is a hack for the electron builder.
.node_modules/
lib/
downloads/
build/
arduino-ide-extension/src/node/resources
arduino-ide-extension/Examples/
!electron/build/
src-gen/
webpack.config.js
gen-webpack.config.js
gen-webpack.node.config.js
.DS_Store
# switching from `electron` to `browser` in dev mode.
.browser_modules
yarn*.log
# For the VS Code extensions used by Theia.
plugins
electron-app/plugins
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces
# any Arduino LS generated log files
inols*.log
# The electron-builder output.
electron-app/dist

11
.prettierignore Normal file
View File

@ -0,0 +1,11 @@
lib
dist
plugins
src-gen
i18n
gen-webpack*
.browser_modules
arduino-ide-extension/src/node/resources
cli-protocol
*color-theme.json
arduino-icons.json

View File

@ -1,7 +0,0 @@
{
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 80,
"endOfLine": "auto"
}

28
.prettierrc.json Normal file
View File

@ -0,0 +1,28 @@
{
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 80,
"endOfLine": "auto",
"overrides": [
{
"files": "*.json",
"options": {
"tabWidth": 2
}
},
{
"files": "*.css",
"options": {
"tabWidth": 4,
"singleQuote": false
}
},
{
"files": "*.html",
"options": {
"tabWidth": 4
}
}
]
}

72
.vscode/launch.json vendored
View File

@ -4,35 +4,33 @@
{
"type": "node",
"request": "launch",
"name": "App (Electron) [Dev]",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"name": "App",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--app-project-path=${workspaceRoot}/electron-app",
"--app-project-path=${workspaceFolder}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--plugins=local-dir:./plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
"--open-devtools",
"--no-ping-timeout",
"--no-ping-timeout"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
"${workspaceFolder}/electron-app/lib/backend/main.js",
"${workspaceFolder}/electron-app/lib/**/*.js",
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
"${workspaceFolder}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
@ -41,33 +39,35 @@
{
"type": "node",
"request": "launch",
"name": "App (Electron)",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"name": "App [Dev]",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--app-project-path=${workspaceRoot}/electron-app",
"--app-project-path=${workspaceFolder}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--plugins=local-dir:./plugins",
"--hosted-plugin-inspect=9339",
"--no-ping-timeout",
"--content-trace",
"--open-devtools",
"--no-ping-timeout"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
"${workspaceFolder}/electron-app/lib/backend/main.js",
"${workspaceFolder}/electron-app/lib/**/*.js",
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
"${workspaceFolder}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
@ -84,7 +84,7 @@
"type": "node",
"request": "launch",
"name": "Run Test [current]",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"--require",
"reflect-metadata/Reflect",
@ -94,8 +94,16 @@
"--colors",
"**/${fileBasenameNoExtension}.js"
],
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"env": {
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json",
"IDE2_TEST": "true"
},
"sourceMaps": true,
"smartStep": true,
@ -107,22 +115,12 @@
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
}
],
"compounds": [
{
"name": "Launch Electron Backend & Frontend",
"configurations": [
"App (Electron)",
"Attach to Electron Frontend"
]
"configurations": ["App", "Attach to Electron Frontend"]
}
]
}

View File

@ -7,6 +7,6 @@
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"source.fixAll.eslint": "explicit"
}
}

18
.vscode/tasks.json vendored
View File

@ -2,10 +2,13 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino IDE - Rebuild Electron App",
"label": "Rebuild App",
"type": "shell",
"command": "yarn rebuild:browser && yarn rebuild:electron",
"command": "yarn rebuild",
"group": "build",
"options": {
"cwd": "${workspaceFolder}/electron-app"
},
"presentation": {
"reveal": "always",
"panel": "new",
@ -13,7 +16,7 @@
}
},
{
"label": "Arduino IDE - Watch IDE Extension",
"label": "Watch Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@ -24,7 +27,7 @@
}
},
{
"label": "Arduino IDE - Watch Electron App",
"label": "Watch App",
"type": "shell",
"command": "yarn --cwd ./electron-app watch",
"group": "build",
@ -35,12 +38,9 @@
}
},
{
"label": "Arduino IDE - Watch All [Electron]",
"label": "Watch All",
"type": "shell",
"dependsOn": [
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Electron App"
]
"dependsOn": ["Watch Extension", "Watch App"]
}
]
}

View File

@ -2,9 +2,11 @@
# Arduino IDE 2.x
[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
[![Build status](https://github.com/arduino/arduino-ide/actions/workflows/build.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/build.yml)
[![Check JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml)
[![Test JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml)
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the [repository of the 1.x version](https://github.com/arduino/Arduino).
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.

View File

@ -55,12 +55,14 @@ The Config Service knows about your system, like for example the default sketch
- checking whether a file is in a data or sketch directory
### `"arduino"` configuration in the `package.json`:
- `"cli"`:
- `"version"` type `string` | `{ owner: string, repo: string, commitish?: string }`: if the type is a `string` and is a valid semver, it will get the corresponding [released](https://github.com/arduino/arduino-cli/releases) CLI. If the type is `string` and is a [date in `YYYYMMDD`](https://arduino.github.io/arduino-cli/latest/installation/#nightly-builds) format, it will get a nightly CLI. If the type is an object, a CLI, build from the sources in the `owner/repo` will be used. If `commitish` is not defined, the HEAD of the default branch will be used. In any other cases an error is thrown.
- `"cli"`:
- `"version"` type `string` | `{ owner: string, repo: string, commitish?: string }`: if the type is a `string` and is a valid semver, it will get the corresponding [released](https://github.com/arduino/arduino-cli/releases) CLI. If the type is `string` and is a [date in `YYYYMMDD`](https://arduino.github.io/arduino-cli/latest/installation/#nightly-builds) format, it will get a nightly CLI. If the type is an object, a CLI, build from the sources in the `owner/repo` will be used. If `commitish` is not defined, the HEAD of the default branch will be used. In any other cases an error is thrown.
#### Rebuild gRPC protocol interfaces
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol`
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol`
### Update **clangd** and **ClangFormat**
@ -72,11 +74,13 @@ The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangF
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
- load it
- edit the icons as needed
- !! download the **new** `arduino-icons.json` file and put it in this repo
- Click on "Generate Font" in Icomoon, then download
- place the updated fonts in the `src/style/fonts` directory
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
- load it
- edit the icons as needed
- !! download the **new** `arduino-icons.json` file and put it in this repo
- Click on "Generate Font" in Icomoon, then download
- place the updated fonts in the `src/style/fonts` directory

View File

@ -1,83 +1,86 @@
{
"name": "arduino-ide-extension",
"version": "2.1.0",
"version": "2.3.7",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn download-examples",
"clean": "rimraf lib",
"compose-changelog": "node ./scripts/compose-changelog.js",
"download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js",
"copy-i18n": "npx ncp ../i18n ./build/i18n",
"copy-i18n": "ncp ../i18n ./src/node/resources/i18n",
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "eslint",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"lint": "eslint .",
"prebuild": "rimraf lib",
"build": "tsc",
"build:dev": "yarn build",
"postbuild": "ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/",
"watch": "tsc -w",
"test": "mocha \"./lib/test/**/*.test.js\"",
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
"test": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.test.js\"",
"test:slow": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.slow-test.js\" --slow 5000"
},
"dependencies": {
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.31.1",
"@theia/core": "1.31.1",
"@theia/debug": "1.31.1",
"@theia/editor": "1.31.1",
"@theia/electron": "1.31.1",
"@theia/filesystem": "1.31.1",
"@theia/keymaps": "1.31.1",
"@theia/markers": "1.31.1",
"@theia/messages": "1.31.1",
"@theia/monaco": "1.31.1",
"@theia/monaco-editor-core": "1.67.2",
"@theia/navigator": "1.31.1",
"@theia/outline-view": "1.31.1",
"@theia/output": "1.31.1",
"@theia/plugin-ext": "1.31.1",
"@theia/preferences": "1.31.1",
"@theia/scm": "1.31.1",
"@theia/search-in-workspace": "1.31.1",
"@theia/terminal": "1.31.1",
"@theia/typehierarchy": "1.31.1",
"@theia/workspace": "1.31.1",
"@grpc/grpc-js": "^1.8.14",
"@theia/application-package": "1.57.0",
"@theia/core": "1.57.0",
"@theia/debug": "1.57.0",
"@theia/editor": "1.57.0",
"@theia/electron": "1.57.0",
"@theia/filesystem": "1.57.0",
"@theia/keymaps": "1.57.0",
"@theia/markers": "1.57.0",
"@theia/messages": "1.57.0",
"@theia/monaco": "1.57.0",
"@theia/monaco-editor-core": "1.83.101",
"@theia/navigator": "1.57.0",
"@theia/outline-view": "1.57.0",
"@theia/output": "1.57.0",
"@theia/plugin-ext": "1.57.0",
"@theia/plugin-ext-vscode": "1.57.0",
"@theia/preferences": "1.57.0",
"@theia/scm": "1.57.0",
"@theia/search-in-workspace": "1.57.0",
"@theia/terminal": "1.57.0",
"@theia/test": "1.57.0",
"@theia/typehierarchy": "1.57.0",
"@theia/workspace": "1.57.0",
"@tippyjs/react": "^4.2.5",
"@types/auth0-js": "^9.14.0",
"@types/auth0-js": "^9.21.3",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
"@types/jsdom": "^21.1.1",
"@types/lodash.debounce": "^4.0.6",
"@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/temp": "^0.8.34",
"@types/which": "^1.3.1",
"@vscode/debugprotocol": "^1.51.0",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"auth0-js": "^9.14.0",
"auth0-js": "^9.23.2",
"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",
"deepmerge": "^4.2.2",
"dompurify": "^2.4.7",
"drivelist": "^9.2.4",
"electron-updater": "^4.6.5",
"fast-deep-equal": "^3.1.3",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
"glob": "^7.1.6",
"fqbn": "^1.0.5",
"glob": "10.4.4",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-online": "^9.0.1",
"is-online": "^10.0.0",
"js-yaml": "^3.13.1",
"jsdom": "^21.1.1",
"jsonc-parser": "^2.2.0",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2",
@ -85,49 +88,49 @@
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"node-fetch": "^2.6.1",
"node-log-rotate": "^0.1.5",
"open": "^8.0.6",
"p-debounce": "^2.1.0",
"p-queue": "^2.4.2",
"process": "^0.11.10",
"ps-tree": "^1.2.0",
"query-string": "^7.0.1",
"react-disable": "^0.1.1",
"react-markdown": "^8.0.0",
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2",
"react-tabs": "^6.1.0",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"which": "^1.3.1"
"util": "^0.12.5",
"vscode-arduino-api": "^0.1.2"
},
"devDependencies": {
"@octokit/rest": "^18.12.0",
"@types/chai": "^4.2.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/mocha": "^10.0.0",
"@types/react-window": "^1.8.5",
"@xhmikosr/downloader": "^13.0.1",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"cross-env": "^7.0.3",
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc_tools_node_protoc_ts": "^4.1.0",
"mocha": "^7.0.0",
"grpc_tools_node_protoc_ts": "^5.3.3",
"mocha": "^10.2.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",
"yargs": "^11.1.0"
"rimraf": "^5.0.0"
},
"optionalDependencies": {
"grpc-tools": "^1.9.0"
"@pingghost/protoc": "^1.0.2",
"grpc-tools": "^1.12.4"
},
"mocha": {
"require": [
@ -147,6 +150,9 @@
"examples"
],
"theiaExtensions": [
{
"preload": "lib/electron-browser/preload"
},
{
"backend": "lib/node/arduino-ide-backend-module",
"frontend": "lib/browser/arduino-ide-frontend-module"
@ -157,22 +163,29 @@
{
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
"frontendElectron": "lib/electron-browser/electron-arduino-module"
},
{
"electronMain": "lib/electron-main/arduino-electron-main-module"
}
],
"arduino": {
"cli": {
"version": "0.32.2"
"arduino-cli": {
"version": "1.2.0"
},
"fwuploader": {
"version": "2.2.2"
"arduino-fwuploader": {
"version": "2.4.1"
},
"arduino-language-server": {
"version": {
"owner": "arduino",
"repo": "arduino-language-server",
"commitish": "05ec308"
}
},
"clangd": {
"version": "14.0.0"
},
"languageServer": {
"version": "0.7.4"
}
}
}

View File

@ -34,7 +34,7 @@
}, '');
const args = process.argv.slice(2);
if (args.length == 0) {
if (args.length === 0) {
console.error('Missing argument to destination file');
process.exit(1);
}

View File

@ -2,7 +2,6 @@
(async () => {
const path = require('path');
const shell = require('shelljs');
const semver = require('semver');
const moment = require('moment');
const downloader = require('./downloader');
@ -19,7 +18,7 @@
return undefined;
}
const { cli } = arduino;
const cli = arduino['arduino-cli'];
if (!cli) {
return undefined;
}
@ -29,14 +28,20 @@
})();
if (!version) {
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
shell.exit(1);
console.log(`Could not retrieve CLI version info from the 'package.json'.`);
process.exit(1);
}
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
const destinationPath = path.join(buildFolder, cliName);
const destinationPath = path.join(resourcesFolder, cliName);
if (typeof version === 'string') {
const suffix = (() => {
@ -65,24 +70,24 @@
}
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
console.log(`The CLI is not available for ${platform} ${arch}.`);
process.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
shell.echo(
console.log(
`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
shell.echo(
console.log(
`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else {
shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1);
console.log(`🔥 Could not interpret 'version': ${version}`);
process.exit(1);
}
} else {
taskBuildFromGit(version, destinationPath, 'CLI');

View File

@ -1,38 +1,61 @@
// @ts-check
// The version to use.
const version = '1.10.0';
const version = '1.10.2';
(async () => {
const os = require('os');
const { promises: fs } = require('fs');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const os = require('node:os');
const {
existsSync,
promises: fs,
mkdirSync,
readdirSync,
cpSync,
} = require('node:fs');
const path = require('node:path');
const { exec } = require('./utils');
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
const destination = path.join(
__dirname,
'..',
'src',
'node',
'resources',
'Examples'
);
if (existsSync(destination)) {
console.log(
`Skipping Git checkout of the examples because the repository already exists: ${destination}`
);
return;
}
if (
shell.exec(
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
).code !== 0
) {
shell.exit(1);
}
const repository = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-examples-')
);
if (
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
.code !== 0
) {
shell.exit(1);
}
exec(
'git',
['clone', 'https://github.com/arduino/arduino-examples.git', repository],
{ logStdout: true }
);
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
exec(
'git',
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
{ logStdout: true }
);
mkdirSync(destination, { recursive: true });
const examplesPath = path.join(repository, 'examples');
const exampleResources = readdirSync(examplesPath);
for (const exampleResource of exampleResources) {
cpSync(
path.join(examplesPath, exampleResource),
path.join(destination, exampleResource),
{ recursive: true }
);
}
const isSketch = async (pathLike) => {
try {
@ -92,5 +115,5 @@ const version = '1.10.0';
JSON.stringify(examples, null, 2),
{ encoding: 'utf8' }
);
shell.echo(`Generated output to ${path.join(destination, 'examples.json')}`);
console.log(`Generated output to ${path.join(destination, 'examples.json')}`);
})();

View File

@ -1,12 +1,10 @@
// @ts-check
(async () => {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
const path = require('node:path');
const semver = require('semver');
const downloader = require('./downloader');
const { taskBuildFromGit } = require('./utils');
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
@ -19,7 +17,7 @@
return undefined;
}
const { fwuploader } = arduino;
const fwuploader = arduino['arduino-fwuploader'];
if (!fwuploader) {
return undefined;
}
@ -29,24 +27,37 @@
})();
if (!version) {
shell.echo(
console.log(
`Could not retrieve Firmware Uploader version info from the 'package.json'.`
);
shell.exit(1);
process.exit(1);
}
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const fwuploderName = `arduino-fwuploader${
platform === 'win32' ? '.exe' : ''
}`;
const destinationPath = path.join(buildFolder, fwuploderName);
const destinationPath = path.join(resourcesFolder, fwuploderName);
if (typeof version === 'string') {
const suffix = (() => {
switch (platform) {
case 'darwin':
return 'macOS_64bit.tar.gz';
switch (arch) {
case 'arm64':
return 'macOS_ARM64.tar.gz';
case 'x64':
return 'macOS_64bit.tar.gz';
default:
return undefined;
}
case 'win32':
return 'Windows_64bit.zip';
case 'linux': {
@ -66,14 +77,14 @@
}
})();
if (!suffix) {
shell.echo(
console.log(
`The Firmware Uploader is not available for ${platform} ${arch}.`
);
shell.exit(1);
process.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-fwuploader/arduino-fwuploader_${version}_${suffix}`;
shell.echo(
console.log(
`📦 Identified released version of the Firmware Uploader. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(
@ -82,85 +93,10 @@
'arduino-fwuploader'
);
} else {
shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1);
console.log(`🔥 Could not interpret 'version': ${version}`);
process.exit(1);
}
} else {
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(
`Building Firmware Uploader from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
`Skipping the Firmware Uploader build because it already exists: ${destinationPath}`
);
return;
}
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning Firmware Uploader source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned Firmware Uploader repo.');
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (
shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0
) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the Firmware Uploader...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Firmware Uploader build done.');
if (!fs.existsSync(path.join(tempRepoPath, fwuploderName))) {
shell.echo(
`Could not find the Firmware Uploader at ${path.join(
tempRepoPath,
fwuploderName
)}.`
);
shell.exit(1);
}
const builtFwUploaderPath = path.join(tempRepoPath, fwuploderName);
shell.echo(
`>>> Copying Firmware Uploader from ${builtFwUploaderPath} to ${destinationPath}...`
);
if (shell.cp(builtFwUploaderPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the Firmware Uploader.`);
shell.echo('<<< Verifying Firmware Uploader...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified Firmware Uploader.');
taskBuildFromGit(version, destinationPath, 'Firmware Uploader');
}
})();

View File

@ -5,7 +5,6 @@
(() => {
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const { goBuildFromGit } = require('./utils');
@ -16,7 +15,8 @@
const { arduino } = pkg;
if (!arduino) return [undefined, undefined];
const { languageServer, clangd } = arduino;
const { clangd } = arduino;
const languageServer = arduino['arduino-language-server'];
if (!languageServer) return [undefined, undefined];
if (!clangd) return [undefined, undefined];
@ -24,20 +24,20 @@
})();
if (!DEFAULT_LS_VERSION) {
shell.echo(
console.log(
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
);
shell.exit(1);
process.exit(1);
}
if (!DEFAULT_CLANGD_VERSION) {
shell.echo(
console.log(
`Could not retrieve clangd version info from the 'package.json'.`
);
shell.exit(1);
process.exit(1);
}
const yargs = require('yargs')
const yargs = require('@theia/core/shared/yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_LS_VERSION,
@ -62,35 +62,50 @@
const force = yargs['force-download'];
const { platform, arch } = process;
const platformArch = platform + '-' + arch;
const build = path.join(__dirname, '..', 'build');
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const lsExecutablePath = path.join(
build,
resourcesFolder,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'darwin-arm64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
lsSuffix = 'macOS_ARM64.tar.gz';
clangdSuffix = 'macOS_ARM64';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'linux-arm64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
lsSuffix = 'Linux_ARM64.tar.gz';
clangdSuffix = 'Linux_ARM64';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
clangdExecutablePath = path.join(resourcesFolder, 'clangd.exe');
clangFormatExecutablePath = path.join(
resourcesFolder,
'clang-format.exe'
);
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
@ -98,10 +113,10 @@
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
}
if (!lsSuffix || !clangdSuffix) {
shell.echo(
console.log(
`The arduino-language-server is not available for ${platform} ${arch}.`
);
shell.exit(1);
process.exit(1);
}
if (typeof lsVersion === 'string') {
@ -110,20 +125,31 @@
? 'nightly/arduino-language-server'
: 'arduino-language-server_' + lsVersion
}_${lsSuffix}`;
downloader.downloadUnzipAll(lsUrl, build, lsExecutablePath, force);
downloader.downloadUnzipAll(
lsUrl,
resourcesFolder,
lsExecutablePath,
force
);
} else {
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
}
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
downloader.downloadUnzipAll(
clangdUrl,
resourcesFolder,
clangdExecutablePath,
force,
{
strip: 1,
}
); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
resourcesFolder,
clangFormatExecutablePath,
force,
{

View File

@ -1,21 +1,19 @@
// @ts-check
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untargz = require('decompress-targz');
const untarbz2 = require('decompress-tarbz2');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
process.on('unhandledRejection', (reason) => {
console.log(String(reason));
process.exit(1);
});
process.on('uncaughtException', (error) => {
shell.echo(String(error));
shell.exit(1);
throw error;
console.log(String(error));
process.exit(1);
});
/**
@ -31,54 +29,42 @@ exports.downloadUnzipFile = async (
force = false
) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
console.log(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
fs.rmSync(targetFile, { recursive: true, force: true });
fs.rmSync(downloads, { recursive: true, force: true });
shell.echo(`>>> Downloading from '${url}'...`);
console.log(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
console.log(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
console.log('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [unzip(), untargz(), untarbz2()],
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
console.log('Error ocurred while decompressing the archive.');
process.exit(1);
}
const fileIndex = files.findIndex((f) => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(
console.log(
`The downloaded artifact does not contain any file with prefix ${filePrefix}.`
);
shell.exit(1);
process.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
console.log('<<< Decompressing succeeded.');
if (
shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile)
.code !== 0
) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
fs.renameSync(path.join(downloads, files[fileIndex].path), targetFile);
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
console.log(`Could not find file: ${targetFile}`);
process.exit(1);
}
shell.echo(`Done: ${targetFile}`);
console.log(`Done: ${targetFile}`);
};
/**
@ -86,7 +72,7 @@ exports.downloadUnzipFile = async (
* @param targetDir {string} Directory into which to decompress the archive
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
* @param decompressOptions {import('decompress').DecompressOptions}
* @param decompressOptions {import('decompress').DecompressOptions|undefined} [decompressOptions]
*/
exports.downloadUnzipAll = async (
url,
@ -96,21 +82,16 @@ exports.downloadUnzipAll = async (
decompressOptions = undefined
) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
console.log(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(targetDir)) {
if (shell.mkdir('-p', targetDir).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
fs.mkdirSync(targetDir, { recursive: true });
shell.echo(`>>> Downloading from '${url}'...`);
console.log(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
console.log(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
console.log('>>> Decompressing...');
let options = {
plugins: [unzip(), untargz(), untarbz2()],
};
@ -119,14 +100,27 @@ exports.downloadUnzipAll = async (
}
const files = await decompress(data, targetDir, options);
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
console.log('Error ocurred while decompressing the archive.');
process.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
console.log('<<< Decompressing succeeded.');
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
console.log(`Could not find file: ${targetFile}`);
process.exit(1);
}
shell.echo(`Done: ${targetFile}`);
console.log(`Done: ${targetFile}`);
};
/**
* @param {string} url
* @returns {Promise<import('node:buffer').Buffer>}
*/
async function download(url) {
const { default: download } = await import('@xhmikosr/downloader');
/** @type {import('node:buffer').Buffer} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data = await download(url);
return data;
}

View File

@ -1,82 +1,88 @@
// @ts-check
(async () => {
const os = require('node:os');
const path = require('node:path');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const { mkdirSync, promises: fs, rmSync, existsSync } = require('node:fs');
const { exec } = require('./utils');
const { glob } = require('glob');
const { SemVer, gte, valid: validSemVer, eq } = require('semver');
// Use a node-protoc fork until apple arm32 is supported
// https://github.com/YePpHa/node-protoc/pull/10
const protoc = path.dirname(require('@pingghost/protoc/protoc'));
const os = require('os');
const path = require('path');
const glob = require('glob');
const { v4 } = require('uuid');
const shell = require('shelljs');
const protoc = path.dirname(require('protoc/protoc'));
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${protoc}`;
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
const { owner, repo, commitish } = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
console.log(`Could not parse the 'package.json'.`);
process.exit(1);
}
const { owner, repo, commitish } = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
shell.echo(`Could not parse the 'package.json'.`);
shell.exit(1);
}
const { arduino } = pkg;
if (!arduino) {
return { owner: 'arduino', repo: 'arduino-cli' };
}
const { cli } = arduino;
if (!cli) {
return { owner: 'arduino', repo: 'arduino-cli' };
}
const { version } = cli;
if (!version) {
return { owner: 'arduino', repo: 'arduino-cli' };
}
if (typeof version === 'string') {
return { owner: 'arduino', repo: 'arduino-cli' };
}
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
return { owner, repo, commitish };
})();
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(`>>> Cloning repository from '${url}'...`);
if (shell.exec(`git clone ${url} ${repository}`).code !== 0) {
shell.exit(1);
const defaultVersion = {
owner: 'arduino',
repo: 'arduino-cli',
commitish: undefined,
};
const { arduino } = pkg;
if (!arduino) {
return defaultVersion;
}
shell.echo(`<<< Repository cloned.`);
const { platform } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const versionJson = shell.exec(`${cli} version --format json`).trim();
if (!versionJson) {
shell.echo(`Could not retrieve the CLI version from ${cli}.`);
shell.exit(1);
const cli = arduino['arduino-cli'];
if (!cli) {
return defaultVersion;
}
// As of today (28.01.2021), the `VersionString` can be one of the followings:
// - `nightly-YYYYMMDD` stands for the nightly build, we use the , the `commitish` from the `package.json` to check out the code.
// - `0.0.0-git` for local builds, we use the `commitish` from the `package.json` to check out the code and generate the APIs.
// - `git-snapshot` for local build executed via `task build`. We do not do this.
// - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files.
/*
const { version } = cli;
if (!version) {
return defaultVersion;
}
if (typeof version === 'string') {
return defaultVersion;
}
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
console.log(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
process.exit(1);
}
if (!repo) {
console.log(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
process.exit(1);
}
return { owner, repo, commitish };
})();
const { platform } = process;
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const cli = path.join(
resourcesFolder,
`arduino-cli${platform === 'win32' ? '.exe' : ''}`
);
const versionJson = exec(cli, ['version', '--format', 'json'], {
logStdout: true,
}).trim();
if (!versionJson) {
console.log(`Could not retrieve the CLI version from ${cli}.`);
process.exit(1);
}
// As of today (28.01.2021), the `VersionString` can be one of the followings:
// - `nightly-YYYYMMDD` stands for the nightly build, we use the , the `commitish` from the `package.json` to check out the code.
// - `0.0.0-git` for local builds, we use the `commitish` from the `package.json` to check out the code and generate the APIs.
// - `git-snapshot` for local build executed via `task build`. We do not do this.
// - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files.
/*
{
"Application": "arduino-cli",
"VersionString": "nightly-20210126",
@ -84,73 +90,200 @@
"Status": "alpha",
"Date": "2021-01-26T01:46:31Z"
}
*/
const versionObject = JSON.parse(versionJson);
const version = versionObject.VersionString;
if (version && !version.startsWith('nightly-') && version !== '0.0.0-git' && version !== 'git-snapshot') {
shell.echo(`>>> Checking out tagged version: '${version}'...`);
shell.exec(`git -C ${repository} fetch --all --tags`);
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out tagged version: '${commitish}'.`);
*/
const versionObject = JSON.parse(versionJson);
async function globProtos(folder, pattern = '**/*.proto') {
let protos = [];
try {
const matches = await glob(pattern, { cwd: folder });
protos = matches.map((filename) => path.join(folder, filename));
} catch (error) {
console.log(error.stack ?? error.message);
}
return protos;
}
async function getProtosFromRepo(
commitish = '',
version = '',
owner = 'arduino',
repo = 'arduino-cli'
) {
const repoFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-'));
const url = `https://github.com/${owner}/${repo}.git`;
console.log(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repoFolder], { logStdout: true });
console.log(`<<< Repository cloned.`);
if (validSemVer(version)) {
let versionTag = version;
// https://github.com/arduino/arduino-cli/pull/2374
if (
gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))
) {
versionTag = `v${version}`;
}
console.log(`>>> Checking out tagged version: '${versionTag}'...`);
exec('git', ['-C', repoFolder, 'fetch', '--all', '--tags'], {
logStdout: true,
});
exec(
'git',
['-C', repoFolder, 'checkout', `tags/${versionTag}`, '-b', versionTag],
{ logStdout: true }
);
console.log(`<<< Checked out tagged version: '${versionTag}'.`);
} else if (commitish) {
shell.echo(`>>> Checking out commitish from 'package.json': '${commitish}'...`);
if (shell.exec(`git -C ${repository} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish from 'package.json': '${commitish}'.`);
} else if (versionObject.Commit) {
shell.echo(`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`);
if (shell.exec(`git -C ${repository} checkout ${versionObject.Commit}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`);
console.log(`>>> Checking out commitish: '${commitish}'...`);
exec('git', ['-C', repoFolder, 'checkout', commitish], {
logStdout: true,
});
console.log(`<<< Checked out commitish: '${commitish}'.`);
} else {
shell.echo(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
console.log(
`WARN: no 'git checkout'. Generating from the HEAD revision.`
);
}
shell.echo('>>> Generating TS/JS API from:');
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
shell.exit(1);
const rpcFolder = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-cli-rpc')
);
// Copy the the repository rpc folder so we can remove the repository
await fs.cp(path.join(repoFolder, 'rpc'), path.join(rpcFolder), {
recursive: true,
});
rmSync(repoFolder, { recursive: true, maxRetries: 5, force: true });
// Patch for https://github.com/arduino/arduino-cli/issues/2755
// Google proto files are removed from source since v1.1.0
if (!existsSync(path.join(rpcFolder, 'google'))) {
// Include packaged google proto files from v1.1.1
// See https://github.com/arduino/arduino-cli/pull/2761
console.log(`>>> Missing google proto files. Including from v1.1.1...`);
const v111ProtoFolder = await getProtosFromZip('1.1.1');
// Create an return a folder name google in rpcFolder
const googleFolder = path.join(rpcFolder, 'google');
await fs.cp(path.join(v111ProtoFolder, 'google'), googleFolder, {
recursive: true,
});
console.log(`<<< Included google proto files from v1.1.1.`);
}
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
shell.mkdir('-p', out);
return rpcFolder;
}
const protos = await new Promise(resolve =>
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
if (error) {
shell.echo(error.stack);
resolve([]);
return;
}
resolve(matches.map(filename => path.join(rpc, filename)));
}));
if (!protos || protos.length === 0) {
shell.echo(`Could not find any .proto files under ${rpc}.`);
shell.exit(1);
async function getProtosFromZip(version) {
if (!version) {
console.log(`Could not download proto files: CLI version not provided.`);
process.exit(1);
}
console.log(`>>> Downloading proto files from zip for ${version}.`);
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_proto.zip`;
const protos = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-cli-proto')
);
const { default: download } = await import('@xhmikosr/downloader');
/** @type {import('node:buffer').Buffer} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data = await download(url);
await decompress(data, protos, {
plugins: [unzip()],
filter: (file) => file.path.endsWith('.proto'),
});
console.log(
`<<< Finished downloading and extracting proto files for ${version}.`
);
return protos;
}
let protosFolder;
if (commitish) {
protosFolder = await getProtosFromRepo(commitish, undefined, owner, repo);
} else if (
versionObject.VersionString &&
validSemVer(versionObject.VersionString)
) {
const version = versionObject.VersionString;
// v1.1.0 does not contains google proto files in zip
// See https://github.com/arduino/arduino-cli/issues/2755
const isV110 = eq(new SemVer(version, { loose: true }), '1.1.0');
protosFolder = isV110
? await getProtosFromRepo(undefined, version)
: await getProtosFromZip(version);
} else if (versionObject.Commit) {
protosFolder = await getProtosFromRepo(versionObject.Commit);
}
if (!protosFolder) {
console.log(`Could not get proto files: missing commitish or version.`);
process.exit(1);
}
const protos = await globProtos(protosFolder);
if (!protos || protos.length === 0) {
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
console.log(`Could not find any .proto files under ${protosFolder}.`);
process.exit(1);
}
console.log('>>> Generating TS/JS API from:');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Must wipe the gen output folder. Otherwise, dangling service implementation remain in IDE2 code,
// although it has been removed from the proto file.
// For example, https://github.com/arduino/arduino-cli/commit/50a8bf5c3e61d5b661ccfcd6a055e82eeb510859.
// rmSync(out, { recursive: true, maxRetries: 5, force: true });
mkdirSync(out, { recursive: true });
try {
// Generate JS code from the `.proto` files.
if (shell.exec(`grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${out} \
--grpc_out=generate_package_definition:${out} \
-I ${rpc} \
${protos.join(' ')}`).code !== 0) {
shell.exit(1);
}
exec(
'grpc_tools_node_protoc',
[
`--js_out=import_style=commonjs,binary:${out}`,
`--grpc_out=generate_package_definition:${out}`,
'-I',
protosFolder,
...protos,
],
{ logStdout: true }
);
// Generate the `.d.ts` files for JS.
if (shell.exec(`protoc \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`)} \
--ts_out=generate_package_definition:${out} \
-I ${rpc} \
${protos.join(' ')}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Generation was successful.');
exec(
path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`),
[
`--plugin=protoc-gen-ts=${path.resolve(
__dirname,
'..',
'node_modules',
'.bin',
`protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`
)}`,
`--ts_out=generate_package_definition:${out}`,
'-I',
protosFolder,
...protos,
],
{ logStdout: true }
);
} catch (error) {
console.log(error);
} finally {
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
}
console.log('<<< Generation was successful.');
})();

View File

@ -1,3 +1,28 @@
// @ts-check
const exec = (
/** @type {string} */ command,
/** @type {readonly string[]} */ args,
/** @type {Partial<import('node:child_process').ExecFileSyncOptionsWithStringEncoding> & { logStdout?: boolean }|undefined} */ options = undefined
) => {
try {
const stdout = require('node:child_process').execFileSync(command, args, {
encoding: 'utf8',
...(options ?? {}),
});
if (options?.logStdout) {
console.log(stdout.trim());
}
return stdout;
} catch (err) {
console.log(
`Failed to execute ${command} with args: ${JSON.stringify(args)}`
);
throw err;
}
};
exports.exec = exec;
/**
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
*
@ -21,90 +46,98 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
};
/**
* The `command` is either `go` or `task`.
* The `command` must be either `'go'` or `'task'`.
* @param {string} command
* @param {{ owner: any; repo: any; commitish: any; }} version
* @param {string} destinationPath
* @param {string} taskName
*/
function buildFromGit(command, version, destinationPath, taskName) {
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const temp = require('temp');
const shell = require('shelljs');
// We assume an object with `owner`, `repo`, commitish?` properties.
if (typeof version !== 'object') {
shell.echo(
console.log(
`Expected a \`{ owner, repo, commitish }\` object. Got <${version}> instead.`
);
}
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
console.log(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
process.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
console.log(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
process.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(
console.log(
`Building ${taskName} from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
console.log(
`Skipping the ${taskName} build because it already exists: ${destinationPath}`
);
return;
}
const buildFolder = path.join(__dirname, '..', 'build');
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
fs.mkdirSync(resourcesFolder, { recursive: true });
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Cloned ${taskName} repo.`);
console.log(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
exec('git', ['clone', url, tempRepoPath], { logStdout: true });
console.log(`<<< Cloned ${taskName} repo.`);
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
console.log(`>>> Checking out ${commitish}...`);
exec('git', ['-C', tempRepoPath, 'checkout', commitish], {
logStdout: true,
});
console.log(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the ${taskName}...`);
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Done ${taskName} build.`);
exec('git', ['-C', tempRepoPath, 'rev-parse', '--short', 'HEAD'], {
logStdout: true,
});
console.log(`>>> Building the ${taskName}...`);
exec(command, ['build'], {
cwd: tempRepoPath,
encoding: 'utf8',
logStdout: true,
});
console.log(`<<< Done ${taskName} build.`);
const binName = path.basename(destinationPath);
if (!fs.existsSync(path.join(tempRepoPath, binName))) {
shell.echo(
console.log(
`Could not find the ${taskName} at ${path.join(tempRepoPath, binName)}.`
);
shell.exit(1);
process.exit(1);
}
const binPath = path.join(tempRepoPath, binName);
shell.echo(
console.log(
`>>> Copying ${taskName} from ${binPath} to ${destinationPath}...`
);
if (shell.cp(binPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the ${taskName}.`);
fs.copyFileSync(binPath, destinationPath);
console.log(`<<< Copied the ${taskName}.`);
shell.echo(`<<< Verifying ${taskName}...`);
console.log(`<<< Verifying ${taskName}...`);
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
process.exit(1);
}
shell.echo(`>>> Verified ${taskName}.`);
console.log(`>>> Verified ${taskName}.`);
}

View File

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

View File

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

View File

@ -5,10 +5,8 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import {
FrontendApplicationContribution,
FrontendApplication as TheiaFrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import {
@ -27,7 +25,10 @@ import { SketchesServiceClientImpl } from './sketches-service-client-impl';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import {
BoardListDumper,
BoardsServiceProvider,
} from './boards/boards-service-provider';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
@ -61,7 +62,6 @@ import {
BoardsConfigDialog,
BoardsConfigDialogProps,
} from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
@ -89,7 +89,6 @@ import {
ArduinoDaemonPath,
ArduinoDaemon,
} from '../common/protocol/arduino-daemon';
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
import {
FrontendConnectionStatusService,
ApplicationConnectionStatusContribution,
@ -100,7 +99,7 @@ import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
} from '@theia/core/lib/browser/connection-status-service';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataMenuUpdater } from './contributions/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core/lib/common/logger';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
@ -123,7 +122,10 @@ import { OpenSketch } from './contributions/open-sketch';
import { Close } from './contributions/close';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import {
CompileSummaryProvider,
VerifySketch,
} from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
@ -175,10 +177,9 @@ import {
import { About } from './contributions/about';
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
import { TabBarRenderer } from './theia/core/tab-bars';
import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug } from './contributions/debug';
import { Debug, DebugDisabledStatusMessageSource } from './contributions/debug';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
@ -208,7 +209,6 @@ import {
MonacoEditorFactory,
MonacoEditorProvider as TheiaMonacoEditorProvider,
} from '@theia/monaco/lib/browser/monaco-editor-provider';
import { StorageWrapper } from './storage-wrapper';
import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
@ -265,13 +265,13 @@ import {
IDEUpdaterDialog,
IDEUpdaterDialogProps,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginEvents } from './hosted/hosted-plugin-events';
import { HostedPluginSupportImpl } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
@ -285,17 +285,13 @@ import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-gen
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './contributions/startup-task';
import { StartupTasksExecutor } from './contributions/startup-tasks-executor';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer';
@ -341,16 +337,6 @@ import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch';
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
@ -361,15 +347,53 @@ import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/
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';
import { StylingParticipant } from '@theia/core/lib/browser/styling-service';
import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu';
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
import { UpdateArduinoState } from './contributions/update-arduino-state';
import { TerminalFrontendContribution } from './theia/terminal/terminal-frontend-contribution';
import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { CommandService } from '@theia/core/lib/common/command';
import { CorePreferences } from '@theia/core/lib/browser/core-preferences';
import { AutoSelectProgrammer } from './contributions/auto-select-programmer';
import { HostedPluginSupport } from './hosted/hosted-plugin-support';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugWidget as TheiaDebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugWidget } from './theia/debug/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import {
VersionWelcomeDialog,
VersionWelcomeDialogProps,
} from './dialogs/version-welcome-dialog';
import { TestViewContribution as TheiaTestViewContribution } from '@theia/test/lib/browser/view/test-view-contribution';
import { TestViewContribution } from './theia/test/test-view-contribution';
// Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487
import('@theia/core/lib/browser/common-frontend-contribution.js').then(
(theiaCommonContribution) => {
theiaCommonContribution['supportCopy'] = true;
theiaCommonContribution['supportCut'] = true;
theiaCommonContribution['supportPaste'] = true;
}
);
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
// Commands, colors, theme adjustments, and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
bind(StylingParticipant).toService(ArduinoFrontendContribution);
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
@ -438,13 +462,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
bind(CommandContribution).toService(BoardsServiceProvider);
bind(BoardListDumper).toSelf().inSingletonScope();
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution)
.to(BoardsDataMenuUpdater)
.inSingletonScope();
bind(BoardsDataStore).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsDataStore);
bind(CommandContribution).toService(BoardsDataStore);
bind(StartupTaskProvider).toService(BoardsDataStore); // to inherit the boards config options, programmer, etc in a new window
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
@ -471,7 +496,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: nls.localize(
@ -530,15 +554,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
WorkspaceVariableContribution
);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution)
@ -722,7 +737,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
Contribution.configure(bind, StartupTasks);
Contribution.configure(bind, StartupTasksExecutor);
Contribution.configure(bind, IndexesUpdateProgress);
Contribution.configure(bind, Daemon);
Contribution.configure(bind, FirstStartupInstaller);
@ -743,10 +758,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, Account);
Contribution.configure(bind, CloudSketchbookContribution);
Contribution.configure(bind, CreateCloudCopy);
Contribution.configure(bind, UpdateArduinoState);
Contribution.configure(bind, BoardsDataMenuUpdater);
Contribution.configure(bind, AutoSelectProgrammer);
bind(CompileSummaryProvider).toService(VerifySketch);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
bind(DebugDisabledStatusMessageSource).toService(Debug);
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
@ -789,20 +811,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
const iconThemeService =
context.container.get<IconThemeService>(IconThemeService);
const selectionService =
context.container.get<SelectionService>(SelectionService);
const commandService =
context.container.get<CommandService>(CommandService);
const corePreferences =
context.container.get<CorePreferences>(CorePreferences);
return new TabBarRenderer(
contextMenuRenderer,
decoratorService,
iconThemeService
iconThemeService,
selectionService,
commandService,
corePreferences
);
});
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
// Silent the badge decoration in the Explorer view.
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
@ -835,6 +859,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// To be able to use a `launch.json` from outside of the workspace.
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To update the currently selected debug config <select> option when starting a debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// Customized debug widget with its customized config <select> to update it programmatically.
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: TheiaDebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolBar).toSelf();
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf(); // with the patched select
child // use the customized one in the Theia DI
.bind(TheiaDebugConfigurationWidget)
.toService(DebugConfigurationWidget);
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
@ -871,9 +917,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
),
});
bind(StorageWrapper).toSelf().inSingletonScope();
bind(CommandContribution).toService(StorageWrapper);
bind(NotificationManager).toSelf().inSingletonScope();
rebind(TheiaNotificationManager).toService(NotificationManager);
bind(NotificationsRenderer).toSelf().inSingletonScope();
@ -944,6 +987,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'IDEUpdater',
});
bind(VersionWelcomeDialog).toSelf().inSingletonScope();
bind(VersionWelcomeDialogProps).toConstantValue({
title: 'VersionWelcomeDialog',
});
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
@ -966,8 +1014,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
.inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginSupportImpl).toSelf().inSingletonScope();
bind(HostedPluginSupport).toService(HostedPluginSupportImpl);
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupportImpl);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
@ -982,9 +1031,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// 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(FrontendApplicationContribution).toService(CleanupObsoleteThemes);
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
@ -998,37 +1045,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
// patched the debugger for `cortex-debug@1.5.1`
// https://github.com/eclipse-theia/theia/issues/11871
// https://github.com/eclipse-theia/theia/issues/11879
// https://github.com/eclipse-theia/theia/issues/11880
// https://github.com/eclipse-theia/theia/issues/11885
// https://github.com/eclipse-theia/theia/issues/11886
// https://github.com/eclipse-theia/theia/issues/11916
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
bind(DebugToolbar).toSelf().inSingletonScope();
rebind(TheiaDebugToolbar).toService(DebugToolbar);
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: DebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolbar).toSelf(); // patched toolbar
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf();
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();
bind(SidebarBottomMenuWidget).toSelf();
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
@ -1043,4 +1061,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// https://github.com/arduino/arduino-ide/issues/437
bind(FileResourceResolver).toSelf().inSingletonScope();
rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
// Full control over the editor context menu to filter undesired menu items contributed by Theia.
// https://github.com/arduino/arduino-ide/issues/1394
// https://github.com/arduino/arduino-ide/pull/2027#pullrequestreview-1414246614
bind(MonacoEditorMenuContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorMenuContribution).toService(
MonacoEditorMenuContribution
);
// Patch terminal issues.
bind(TerminalFrontendContribution).toSelf().inSingletonScope();
rebind(TheiaTerminalFrontendContribution).toService(
TerminalFrontendContribution
);
// Hides the Test Explorer from the side-bar
bind(TestViewContribution).toSelf().inSingletonScope();
rebind(TheiaTestViewContribution).toService(TestViewContribution);
});

View File

@ -1,12 +1,15 @@
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceContribution,
PreferenceProxy,
PreferenceSchema,
PreferenceService,
createPreferenceProxy,
} from '@theia/core/lib/browser/preferences';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls';
import { PreferenceSchemaProperty } from '@theia/core/lib/common/preferences/preference-schema';
import { interfaces } from '@theia/core/shared/inversify';
import { serialMonitorWidgetLabel } from '../common/nls';
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
export enum UpdateChannel {
@ -31,7 +34,7 @@ export const ErrorRevealStrategyLiterals = [
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export type ErrorRevealStrategy = (typeof ErrorRevealStrategyLiterals)[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
@ -40,236 +43,295 @@ export namespace ErrorRevealStrategy {
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export type MonitorWidgetDockPanel = Extract<
ApplicationShell.Area,
'bottom' | 'right'
>;
export const defaultMonitorWidgetDockPanel: MonitorWidgetDockPanel = 'bottom';
export function isMonitorWidgetDockPanel(
arg: unknown
): arg is MonitorWidgetDockPanel {
return arg === 'bottom' || arg === 'right';
}
export const defaultAsyncWorkers = 0 as const;
export const minAsyncWorkers = defaultAsyncWorkers;
export const maxAsyncWorkers = 8 as const;
type StrictPreferenceSchemaProperties<T extends object> = {
[p in keyof T]: PreferenceSchemaProperty;
};
type ArduinoPreferenceSchemaProperties =
StrictPreferenceSchemaProperties<ArduinoConfiguration> & {
'arduino.window.zoomLevel': PreferenceSchemaProperty;
};
const properties: ArduinoPreferenceSchemaProperties = {
'arduino.language.log': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.log',
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default."
),
default: false,
},
'arduino.language.realTimeDiagnostics': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.realTimeDiagnostics',
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
),
default: false,
},
'arduino.language.asyncWorkers': {
type: 'number',
description: nls.localize(
'arduino/preferences/language.asyncWorkers',
'Number of async workers used by the Arduino Language Server (clangd). Background index also uses this many workers. The minimum value is 0, and the maximum is 8. When it is 0, the language server uses all available cores. The default value is 0.'
),
minimum: minAsyncWorkers,
maximum: maxAsyncWorkers,
default: defaultAsyncWorkers,
},
'arduino.compile.verbose': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.verbose',
'True for verbose compile output. False by default'
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/compile.revealRange',
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
'arduino/preferences/compile.warnings',
"Tells gcc which warning level to use. It's 'None' by default"
),
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/upload.verbose',
'True for verbose upload output. False by default.'
),
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
description: nls.localize(
'arduino/preferences/upload.verify',
'After upload, verify that the contents of the memory on the board match the uploaded binary.'
),
},
'arduino.upload.autoVerify': {
type: 'boolean',
default: true,
description: nls.localize(
'arduino/preferences/upload.autoVerify',
"True if the IDE should automatically verify the code before the upload. True by default. When this value is false, IDE does not recompile the code before uploading the binary to the board. It's highly advised to only set this value to false if you know what you are doing."
),
},
'arduino.window.autoScale': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/window.autoScale',
'True if the user interface automatically scales with the font size.'
),
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description: '',
default: 0,
deprecationMessage: nls.localize(
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
),
},
'arduino.ide.updateChannel': {
type: 'string',
enum: Object.values(UpdateChannel) as UpdateChannel[],
default: UpdateChannel.Stable,
description: nls.localize(
'arduino/preferences/ide.updateChannel',
"Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build."
),
},
'arduino.ide.updateBaseUrl': {
type: 'string',
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
type: 'string',
description: nls.localize(
'arduino/preferences/board.certificates',
'List of certificates that can be uploaded to boards'
),
default: '',
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/sketchbook.showAllFiles',
'True to show all sketch files inside the sketch. It is false by default.'
),
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.enabled',
'True if the sketch sync functions are enabled. Defaults to true.'
),
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.pull.warn',
'True if users should be warned before pulling a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.push.warn',
'True if users should be warned before pushing a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.pushpublic.warn',
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.'
),
default: true,
},
'arduino.cloud.sketchSyncEndpoint': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sketchSyncEndpoint',
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
),
default: 'https://api2.arduino.cc/create',
},
'arduino.cloud.sharedSpaceID': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sharedSpaceId',
'The ID of the Arduino Cloud shared space to load the sketchbook from. If empty, your private space is selected.'
),
default: '',
},
'arduino.auth.clientID': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.clientID',
'The OAuth2 client ID.'
),
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.domain',
'The OAuth2 domain.'
),
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.audience',
'The OAuth2 audience.'
),
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.registerUri',
'The URI used to register a new user.'
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
'arduino.checkForUpdates': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/checkForUpdate',
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
),
default: true,
},
'arduino.sketch.inoBlueprint': {
type: 'string',
markdownDescription: nls.localize(
'arduino/preferences/sketch/inoBlueprint',
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
),
default: undefined,
},
'arduino.monitor.dockPanel': {
type: 'string',
enum: ['bottom', 'right'],
markdownDescription: nls.localize(
'arduino/preferences/monitor/dockPanel',
'The area of the application shell where the _{0}_ widget will reside. It is either "bottom" or "right". It defaults to "{1}".',
serialMonitorWidgetLabel,
defaultMonitorWidgetDockPanel
),
default: defaultMonitorWidgetDockPanel,
},
};
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
properties: {
'arduino.language.log': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.log',
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default."
),
default: false,
},
'arduino.language.realTimeDiagnostics': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.realTimeDiagnostics',
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
),
default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.verbose',
'True for verbose compile output. False by default'
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/compile.revealRange',
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
'arduino/preferences/compile.warnings',
"Tells gcc which warning level to use. It's 'None' by default"
),
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/upload.verbose',
'True for verbose upload output. False by default.'
),
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
},
'arduino.window.autoScale': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/window.autoScale',
'True if the user interface automatically scales with the font size.'
),
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description: '',
default: 0,
deprecationMessage: nls.localize(
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
),
},
'arduino.ide.updateChannel': {
type: 'string',
enum: Object.values(UpdateChannel) as UpdateChannel[],
default: UpdateChannel.Stable,
description: nls.localize(
'arduino/preferences/ide.updateChannel',
"Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build."
),
},
'arduino.ide.updateBaseUrl': {
type: 'string',
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
type: 'string',
description: nls.localize(
'arduino/preferences/board.certificates',
'List of certificates that can be uploaded to boards'
),
default: '',
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/sketchbook.showAllFiles',
'True to show all sketch files inside the sketch. It is false by default.'
),
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.enabled',
'True if the sketch sync functions are enabled. Defaults to true.'
),
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.pull.warn',
'True if users should be warned before pulling a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.push.warn',
'True if users should be warned before pushing a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cloud.pushpublic.warn',
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.'
),
default: true,
},
'arduino.cloud.sketchSyncEndpoint': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sketchSyncEndpoint',
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
),
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.clientID',
'The OAuth2 client ID.'
),
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.domain',
'The OAuth2 domain.'
),
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.audience',
'The OAuth2 audience.'
),
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: nls.localize(
'arduino/preferences/auth.registerUri',
'The URI used to register a new user.'
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
'arduino.checkForUpdates': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/checkForUpdate',
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
),
default: true,
},
'arduino.sketch.inoBlueprint': {
type: 'string',
markdownDescription: nls.localize(
'arduino/preferences/sketch/inoBlueprint',
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
),
default: undefined,
},
},
properties,
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.language.realTimeDiagnostics': boolean;
'arduino.language.asyncWorkers': number;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.upload.autoVerify': boolean;
'arduino.window.autoScale': boolean;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
@ -280,14 +342,15 @@ export interface ArduinoConfiguration {
'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEndpoint': string;
'arduino.cloud.sharedSpaceID': string;
'arduino.auth.clientID': string;
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean;
'arduino.monitor.dockPanel': MonitorWidgetDockPanel;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@ -3,19 +3,19 @@ import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
CommandRegistry,
CommandContribution,
} from '@theia/core/lib/common/command';
import {
AuthOptions,
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
authServerPort,
} from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server';
import { AuthOptions } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
@injectable()
@ -61,7 +61,7 @@ export class AuthenticationClientService
setOptions(): Promise<void> {
return this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`,
redirectUri: `http://localhost:${authServerPort}/callback`,
responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'],

View File

@ -1,281 +1,229 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
BoardsService,
BoardsPackage,
Board,
Port,
} from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { Installable, ResponseServiceClient } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { nls } from '@theia/core/lib/common';
import { NotificationCenter } from '../notification-center';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { notEmpty } from '@theia/core/lib/common/objects';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { InstallManually } from '../../common/nls';
interface AutoInstallPromptAction {
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
isAcceptance?: boolean;
key: string;
handler: (...args: unknown[]) => unknown;
}
type AutoInstallPromptActions = AutoInstallPromptAction[];
import { Installable, ResponseServiceClient } from '../../common/protocol';
import {
BoardIdentifier,
BoardsPackage,
BoardsService,
createPlatformIdentifier,
isBoardIdentifierChangeEvent,
PlatformIdentifier,
platformIdentifierEquals,
serializePlatformIdentifier,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* Listens on `BoardsConfigChangeEvent`s, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
// * Cases in which we do not show the auto-install prompt:
// 1. When a related platform is already installed
// 2. When a prompt is already showing in the UI
// 3. When a board is unplugged
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(MessageService)
protected readonly messageService: MessageService;
private readonly messageService: MessageService;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
private readonly responseService: ResponseServiceClient;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
private readonly boardsManagerWidgetContribution: BoardsListWidgetFrontendContribution;
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = [];
// * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
// we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
// an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
// showing again
private portSelectedOnLastRefusal: Port | undefined;
private lastRefusedPackageId: string | undefined;
private readonly installNotificationInfos: Readonly<{
boardName: string;
platformId: string;
notificationId: string;
}>[] = [];
private readonly toDispose = new DisposableCollection();
onStart(): void {
const setEventListeners = () => {
this.boardsServiceClient.onBoardsConfigChanged((config) => {
const { selectedBoard, selectedPort } = config;
const boardWasUnplugged =
!selectedPort && this.portSelectedOnLastRefusal;
this.clearLastRefusedPromptInfo();
if (
boardWasUnplugged ||
!selectedBoard ||
this.promptAlreadyShowingForBoard(selectedBoard)
) {
return;
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.ensureCoreExists(event.selectedBoard);
}
this.ensureCoreExists(selectedBoard, selectedPort);
});
// we "clearRefusedPackageInfo" if a "refused" package is eventually
// installed, though this is not strictly necessary. It's more of a
// cleanup, to ensure the related variables are representative of
// current state.
this.notificationCenter.onPlatformDidInstall((installed) => {
if (this.lastRefusedPackageId === installed.item.id) {
this.clearLastRefusedPromptInfo();
}
});
};
// we should invoke this.ensureCoreExists only once we're sure
// everything has been reconciled
this.boardsServiceClient.reconciled.then(() => {
const { selectedBoard, selectedPort } =
this.boardsServiceClient.boardsConfig;
if (selectedBoard) {
this.ensureCoreExists(selectedBoard, selectedPort);
}
setEventListeners();
}),
this.notificationCenter.onPlatformDidInstall((event) =>
this.clearAllNotificationForPlatform(event.item.id)
),
]);
this.boardsServiceProvider.ready.then(() => {
const { selectedBoard } = this.boardsServiceProvider.boardsConfig;
this.ensureCoreExists(selectedBoard);
});
}
private removeNotificationByBoard(selectedBoard: Board): void {
const index = this.notifications.findIndex((notification) =>
Board.sameAs(notification, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
private async findPlatformToInstall(
selectedBoard: BoardIdentifier
): Promise<BoardsPackage | undefined> {
const platformId = await this.findPlatformIdToInstall(selectedBoard);
if (!platformId) {
return undefined;
}
const id = serializePlatformIdentifier(platformId);
const platform = await this.boardsService.getBoardPackage({ id });
if (!platform) {
console.warn(`Could not resolve platform for ID: ${id}`);
return undefined;
}
if (platform.installedVersion) {
return undefined;
}
return platform;
}
private clearLastRefusedPromptInfo(): void {
this.lastRefusedPackageId = undefined;
this.portSelectedOnLastRefusal = undefined;
}
private setLastRefusedPromptInfo(
packageId: string,
selectedPort?: Port
): void {
this.lastRefusedPackageId = packageId;
this.portSelectedOnLastRefusal = selectedPort;
}
private promptAlreadyShowingForBoard(board: Board): boolean {
return Boolean(
this.notifications.find((notification) =>
Board.sameAs(notification, board)
)
);
}
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => {
const candidate = this.getInstallCandidate(packages, selectedBoard);
if (candidate) {
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
} else {
this.removeNotificationByBoard(selectedBoard);
private async findPlatformIdToInstall(
selectedBoard: BoardIdentifier
): Promise<PlatformIdentifier | undefined> {
const selectedBoardPlatformId = createPlatformIdentifier(selectedBoard);
// The board is installed or the FQBN is available from the `board list watch` for Arduino boards. The latter might change!
if (selectedBoardPlatformId) {
const installedPlatforms =
await this.boardsService.getInstalledPlatforms();
const installedPlatformIds = installedPlatforms
.map((platform) => createPlatformIdentifier(platform.id))
.filter(notEmpty);
if (
installedPlatformIds.every(
(installedPlatformId) =>
!platformIdentifierEquals(
installedPlatformId,
selectedBoardPlatformId
)
)
) {
return selectedBoardPlatformId;
}
});
} else {
// IDE2 knows that selected board is not installed. Look for board `name` match in not yet installed platforms.
// The order should be correct when there is a board name collision (e.g. Arduino Nano RP2040 from Arduino Mbed OS Nano Boards, [DEPRECATED] Arduino Mbed OS Nano Boards). The CLI boosts the platforms, so picking the first name match should be fine.
const platforms = await this.boardsService.search({});
for (const platform of platforms) {
// Ignore installed platforms
if (platform.installedVersion) {
continue;
}
if (
platform.boards.some((board) => board.name === selectedBoard.name)
) {
const platformId = createPlatformIdentifier(platform.id);
if (platformId) {
return platformId;
}
}
}
}
return undefined;
}
private getInstallCandidate(
packages: BoardsPackage[],
selectedBoard: Board
): BoardsPackage | undefined {
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some((board) => board.name === selectedBoard.name)
);
// check if one of the packages for the board is already installed. if so, no hint
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
private async ensureCoreExists(
selectedBoard: BoardIdentifier | undefined
): Promise<void> {
if (!selectedBoard) {
return;
}
const candidate = await this.findPlatformToInstall(selectedBoard);
if (!candidate) {
return;
}
const platformIdToInstall = candidate.id;
const selectedBoardName = selectedBoard.name;
if (
this.installNotificationInfos.some(
({ boardName, platformId }) =>
platformIdToInstall === platformId && selectedBoardName === boardName
)
) {
// Already has a notification for the board with the same platform. Nothing to do.
return;
}
this.clearAllNotificationForPlatform(platformIdToInstall);
// filter the installable (not installed) packages,
// 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(
({ installedVersion }) => !installedVersion
);
return candidates[0];
}
private showAutoInstallPrompt(
candidate: BoardsPackage,
selectedBoard: Board,
selectedPort?: Port
): void {
const candidateName = candidate.name;
const version = candidate.availableVersions[0]
? `[v ${candidate.availableVersions[0]}]`
: '';
const info = this.generatePromptInfoText(
candidateName,
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const message = nls.localize(
'arduino/board/installNow',
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
candidate.name,
version,
selectedBoard.name
);
const actions = this.createPromptActions(candidate);
const onRefuse = () => {
this.setLastRefusedPromptInfo(candidate.id, selectedPort);
};
const handleAction = this.createOnAnswerHandler(actions, onRefuse);
const onAnswer = (answer: string) => {
this.removeNotificationByBoard(selectedBoard);
handleAction(answer);
};
this.messageService
.info(info, ...actions.map((action) => action.key))
.then(onAnswer);
}
private generatePromptInfoText(
candidateName: string,
version: string,
boardName: string
): string {
return nls.localize(
'arduino/board/installNow',
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
candidateName,
version,
boardName
const notificationId = this.notificationId(message, InstallManually, yes);
this.installNotificationInfos.push({
boardName: selectedBoardName,
platformId: platformIdToInstall,
notificationId,
});
const answer = await this.messageService.info(
message,
InstallManually,
yes
);
}
private createPromptActions(
candidate: BoardsPackage
): AutoInstallPromptActions {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const actions: AutoInstallPromptActions = [
{
key: InstallManually,
handler: () => {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh({
query: candidate.name.toLocaleLowerCase(),
type: 'All',
})
);
},
},
{
isAcceptance: true,
key: yes,
handler: () => {
return Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0],
});
},
},
];
return actions;
}
private createOnAnswerHandler(
actions: AutoInstallPromptActions,
onRefuse?: () => void
): (answer: string) => void {
return (answer) => {
const actionToHandle = actions.find((action) => action.key === answer);
actionToHandle?.handler();
if (!actionToHandle?.isAcceptance && onRefuse) {
onRefuse();
if (answer) {
const index = this.installNotificationInfos.findIndex(
({ boardName, platformId }) =>
platformIdToInstall === platformId && selectedBoardName === boardName
);
if (index !== -1) {
this.installNotificationInfos.splice(index, 1);
}
};
if (answer === yes) {
await Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0],
});
return;
}
if (answer === InstallManually) {
this.boardsManagerWidgetContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh({
query: candidate.name.toLocaleLowerCase(),
type: 'All',
})
);
}
}
}
private clearAllNotificationForPlatform(predicatePlatformId: string): void {
// Discard all install notifications for the same platform.
const notificationsLength = this.installNotificationInfos.length;
for (let i = notificationsLength - 1; i >= 0; i--) {
const { notificationId, platformId } = this.installNotificationInfos[i];
if (platformId === predicatePlatformId) {
this.installNotificationInfos.splice(i, 1);
this.notificationManager.clear(notificationId);
}
}
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Info,
});
}
}

View File

@ -0,0 +1,345 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Event } from '@theia/core/lib/common/event';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import {
Board,
BoardIdentifier,
BoardWithPackage,
DetectedPort,
findMatchingPortIndex,
Port,
PortIdentifier,
} from '../../common/protocol/boards-service';
import type { Defined } from '../../common/types';
import { NotificationCenter } from '../notification-center';
import { BoardsConfigDialogState } from './boards-config-dialog';
namespace BoardsConfigComponent {
export interface Props {
/**
* This is not the real config, it's only living in the dialog. Users can change it without update and can cancel any modifications.
*/
readonly boardsConfig: BoardsConfigDialogState;
readonly searchSet: BoardIdentifier[] | undefined;
readonly notificationCenter: NotificationCenter;
readonly appState: FrontendApplicationState;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<
Defined<EditBoardsConfigActionParams['query']>
>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
readonly onBoardSelected: (board: BoardIdentifier) => void;
readonly onPortSelected: (port: PortIdentifier) => void;
readonly searchBoards: (query?: {
query?: string;
}) => Promise<BoardWithPackage[]>;
readonly ports: (
predicate?: (port: DetectedPort) => boolean
) => readonly DetectedPort[];
}
export interface State {
searchResults: Array<BoardWithPackage>;
showAllPorts: boolean;
query: string;
}
}
class Item<T> extends React.Component<{
item: T;
label: string;
selected: boolean;
onClick: (item: T) => void;
missing?: boolean;
details?: string;
title?: string | ((item: T) => string);
}> {
override render(): React.ReactNode {
const { selected, label, missing, details, item } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
let title = this.props.title ?? `${label}${!details ? '' : details}`;
if (typeof title === 'function') {
title = title(item);
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={title}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
private readonly onClick = () => {
this.props.onClick(this.props.item);
};
}
export class BoardsConfigComponent extends React.Component<
BoardsConfigComponent.Props,
BoardsConfigComponent.State
> {
private readonly toDispose: DisposableCollection;
constructor(props: BoardsConfigComponent.Props) {
super(props);
this.state = {
searchResults: [],
showAllPorts: false,
query: '',
};
this.toDispose = new DisposableCollection();
}
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange(async (state) => {
if (state === 'ready') {
const searchResults = await this.queryBoards({});
this.setState({ searchResults });
}
}),
this.props.notificationCenter.onPlatformDidInstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStop(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) => {
if (typeof query === 'string') {
this.setState({ query }, () => this.updateBoards(this.state.query));
}
}),
]);
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
private readonly updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
private readonly queryBoards = async (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
const result = await this.props.searchBoards(options);
const { searchSet } = this.props;
if (searchSet) {
return result.filter((board) =>
searchSet.some(
(restriction) =>
restriction.fqbn === board.fqbn || restriction.name === board.fqbn
)
);
}
return result;
};
private readonly toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
private readonly selectPort = (selectedPort: PortIdentifier) => {
this.props.onPortSelected(selectedPort);
};
private readonly selectBoard = (selectedBoard: BoardWithPackage) => {
this.props.onBoardSelected(selectedBoard);
};
private readonly focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
override render(): React.ReactNode {
return (
<>
{this.renderContainer(
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</>
);
}
private renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
private renderBoards(): React.ReactNode {
const { boardsConfig } = this.props;
const { searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(
boardsConfig.selectedBoard,
searchResults
)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
}
const title = (board: Board.Detailed): string => {
const { details, manuallyInstalled } = board;
let label = board.name;
if (details) {
label += details;
}
if (manuallyInstalled) {
label += nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)');
}
return label;
};
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<Board.Detailed>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
title={title}
/>
));
return (
<React.Fragment>
<div className="search">
<input
type="search"
value={query}
className="theia-input"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
<i className="fa fa-search"></i>
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
private renderPorts(): React.ReactNode {
const predicate = this.state.showAllPorts ? undefined : Port.isVisiblePort;
const detectedPorts = this.props.ports(predicate);
const matchingIndex = findMatchingPortIndex(
this.props.boardsConfig.selectedPort,
detectedPorts
);
return !detectedPorts.length ? (
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{detectedPorts.map((detectedPort, index) => (
<Item<Port>
key={`${Port.keyOf(detectedPort.port)}`}
item={detectedPort.port}
label={Port.toString(detectedPort.port)}
selected={index === matchingIndex}
onClick={this.selectPort}
/>
))}
</div>
);
}
private renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label
title={nls.localize(
'arduino/board/showAllAvailablePorts',
'Shows all available ports when enabled'
)}
>
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
}
}

View File

@ -1,71 +0,0 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceProvider } from './boards-service-provider';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
protected readonly onBoardConfigChangedEmitter =
new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
this.toDispose.pushAll([
this.onBoardConfigChangedEmitter,
this.onFilterTextDidChangeEmitter,
]);
}
search(query: string): void {
this.onFilterTextDidChangeEmitter.fire(query);
}
protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config);
};
protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element;
};
protected render(): React.ReactNode {
return (
<div className="selectBoardContainer">
<BoardsConfig
boardsServiceProvider={this.boardsServiceClient}
notificationCenter={this.notificationCenter}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
/>
</div>
);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@ -1,142 +0,0 @@
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {}
@injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected config: BoardsConfig.Config = {};
constructor(
@inject(BoardsConfigDialogProps)
protected override readonly props: BoardsConfigDialogProps
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
@postConstruct()
protected init(): void {
this.toDispose.push(
this.boardsServiceClient.onBoardsConfigChanged((config) => {
this.config = config;
this.update();
})
);
}
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
override async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
this.widget.search(query);
}
return super.open();
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
nls.localize(
'arduino/board/configDialog1',
'Select both a Board and a Port if you want to upload a sketch.'
),
nls.localize(
'arduino/board/configDialog2',
'If you only select a Board you will be able to compile, but not to upload your sketch.'
),
]) {
const p = document.createElement('div');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(
this.widget.onBoardConfigChanged((config) => {
this.config = config;
this.update();
})
);
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(
'arduino/board/pleasePickBoard',
'Please pick a board connected to the port you have selected.'
);
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
}

View File

@ -0,0 +1,203 @@
import { DialogError, DialogProps } from '@theia/core/lib/browser/dialogs';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import type { Message } from '@theia/core/shared/@phosphor/messaging';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import type { ReactNode } from '@theia/core/shared/react/index';
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import {
BoardIdentifier,
BoardsConfig,
BoardWithPackage,
DetectedPort,
emptyBoardsConfig,
PortIdentifier,
} from '../../common/protocol/boards-service';
import type { Defined } from '../../common/types';
import { NotificationCenter } from '../notification-center';
import { ReactDialog } from '../theia/dialogs/dialogs';
import { BoardsConfigComponent } from './boards-config-component';
import { BoardsServiceProvider } from './boards-service-provider';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {}
export type BoardsConfigDialogState = Omit<BoardsConfig, 'selectedBoard'> & {
selectedBoard: BoardsConfig['selectedBoard'] | BoardWithPackage;
};
@injectable()
export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private readonly onFilterTextDidChangeEmitter: Emitter<
Defined<EditBoardsConfigActionParams['query']>
>;
private readonly onBoardSelected = (board: BoardWithPackage): void => {
this._boardsConfig.selectedBoard = board;
this.update();
};
private readonly onPortSelected = (port: PortIdentifier): void => {
this._boardsConfig.selectedPort = port;
this.update();
};
private readonly setFocusNode = (element: HTMLElement | undefined): void => {
this.focusNode = element;
};
private readonly searchBoards = (options: {
query?: string;
}): Promise<BoardWithPackage[]> => {
return this.boardsServiceProvider.searchBoards(options);
};
private readonly ports = (
predicate?: (port: DetectedPort) => boolean
): readonly DetectedPort[] => {
return this.boardsServiceProvider.boardList.ports(predicate);
};
private _boardsConfig: BoardsConfigDialogState;
/**
* When the dialog's boards result set is limited to a subset of boards when searching, this field is set.
*/
private _searchSet: BoardIdentifier[] | undefined;
private focusNode: HTMLElement | undefined;
constructor(
@inject(BoardsConfigDialogProps)
protected override readonly props: BoardsConfigDialogProps
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this._boardsConfig = emptyBoardsConfig();
this.onFilterTextDidChangeEmitter = new Emitter();
}
@postConstruct()
protected init(): void {
this.boardsServiceProvider.onBoardListDidChange(() => {
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
this.update();
});
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
}
override async open(
disposeOnResolve = true,
params?: EditBoardsConfigActionParams
): Promise<BoardsConfig | undefined> {
this._searchSet = undefined;
this._boardsConfig.selectedBoard =
this.boardsServiceProvider.boardsConfig.selectedBoard;
this._boardsConfig.selectedPort =
this.boardsServiceProvider.boardsConfig.selectedPort;
if (params) {
if (typeof params.query === 'string') {
this.onFilterTextDidChangeEmitter.fire(params.query);
}
if (params.portToSelect) {
this._boardsConfig.selectedPort = params.portToSelect;
}
if (params.boardToSelect) {
this._boardsConfig.selectedBoard = params.boardToSelect;
}
if (params.searchSet) {
this._searchSet = params.searchSet.slice();
}
}
return super.open(disposeOnResolve);
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.update();
}
protected override render(): ReactNode {
return (
<>
<div className="head">
<div className="text">
<div>
{nls.localize(
'arduino/board/configDialog1',
'Select both a Board and a Port if you want to upload a sketch.'
)}
</div>
<div>
{nls.localize(
'arduino/board/configDialog2',
'If you only select a Board you will be able to compile, but not to upload your sketch.'
)}
</div>
</div>
</div>
<div id="select-board-dialog" className="p-Widget ps">
<div className="selectBoardContainer">
<BoardsConfigComponent
boardsConfig={this._boardsConfig}
searchSet={this._searchSet}
onBoardSelected={this.onBoardSelected}
onPortSelected={this.onPortSelected}
notificationCenter={this.notificationCenter}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={
this.onFilterTextDidChangeEmitter.event
}
appState={this.appStateService.state}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
searchBoards={this.searchBoards}
ports={this.ports}
/>
</div>
</div>
</>
);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected override isValid(value: BoardsConfig): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(
'arduino/board/pleasePickBoard',
'Please pick a board connected to the port you have selected.'
);
}
return false;
}
return '';
}
get value(): BoardsConfigDialogState {
return this._boardsConfig;
}
}

View File

@ -1,432 +0,0 @@
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
BoardConfig as ProtocolBoardConfig,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import {
AvailableBoard,
BoardsServiceProvider,
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export type Config = ProtocolBoardConfig;
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
readonly notificationCenter: NotificationCenter;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
searchResults: Array<BoardWithPackage>;
knownPorts: Port[];
showAllPorts: boolean;
query: string;
}
}
export abstract class Item<T> extends React.Component<{
item: T;
label: string;
selected: boolean;
onClick: (item: T) => void;
missing?: boolean;
details?: string;
}> {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
protected onClick = () => {
this.props.onClick(this.props.item);
};
}
export class BoardsConfig extends React.Component<
BoardsConfig.Props,
BoardsConfig.State
> {
protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceProvider;
this.state = {
searchResults: [],
knownPorts: [],
showAllPorts: false,
query: '',
...boardsConfig,
};
}
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange((state) => {
if (state === 'ready') {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
}
}),
this.props.boardsServiceProvider.onAvailablePortsChanged(
({ newState, oldState }) => {
const removedPorts = oldState.filter(
(oldPort) =>
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
);
this.updatePorts(newState, removedPorts);
}
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () =>
this.fireConfigChanged()
);
}
),
this.props.notificationCenter.onPlatformDidInstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStop(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () => this.updateBoards(this.state.query))
),
]);
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected get availableBoards(): AvailableBoard[] {
return this.props.boardsServiceProvider.availableBoards;
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
// Available ports must be sorted in this order:
// 1. Serial with recognized boards
// 2. Serial with guessed boards
// 3. Serial with incomplete boards
// 4. Network with recognized boards
// 5. Other protocols with recognized boards
const ports = (await availablePorts).sort((left: Port, right: Port) => {
if (left.protocol === 'serial' && right.protocol !== 'serial') {
return -1;
} else if (left.protocol !== 'serial' && right.protocol === 'serial') {
return 1;
} else if (left.protocol === 'network' && right.protocol !== 'network') {
return -1;
} else if (left.protocol !== 'network' && right.protocol === 'network') {
return 1;
} else if (left.protocol === right.protocol) {
// We show ports, including those that have guessed
// or unrecognized boards, so we must sort those too.
const leftBoard = this.availableBoards.find(
(board) => board.port === left
);
const rightBoard = this.availableBoards.find(
(board) => board.port === right
);
if (leftBoard && !rightBoard) {
return -1;
} else if (!leftBoard && rightBoard) {
return 1;
} else if (leftBoard?.state! < rightBoard?.state!) {
return -1;
} else if (leftBoard?.state! > rightBoard?.state!) {
return 1;
}
}
return naturalCompare(left.address, right.address);
});
return { knownPorts: ports };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
override render(): React.ReactNode {
return (
<>
{this.renderContainer(
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
<input
type="search"
value={query}
className="theia-input"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
<i className="fa fa-search"></i>
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
protected renderPorts(): React.ReactNode {
let ports = [] as Port[];
if (this.state.showAllPorts) {
ports = this.state.knownPorts;
} else {
ports = this.state.knownPorts.filter(
Port.visiblePorts(this.availableBoards)
);
}
return !ports.length ? (
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{ports.map((port) => (
<Item<Port>
key={`${Port.keyOf(port)}`}
item={port}
label={Port.toString(port)}
selected={Port.sameAs(this.state.selectedPort, port)}
onClick={this.selectPort}
/>
))}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label
title={nls.localize(
'arduino/board/showAllAvailablePorts',
'Shows all available ports when enabled'
)}
>
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
}
}
export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config;
if (Board.is(other)) {
return (
!!selectedBoard &&
Board.equals(other, selectedBoard) &&
Port.sameAs(selectedPort, other.port)
);
}
return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
}
}

View File

@ -1,77 +1,202 @@
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import type {
Command,
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { Event, Emitter } from '@theia/core/lib/common/event';
import {
FrontendApplicationContribution,
LocalStorageService,
} from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils';
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn';
import {
BoardDetails,
BoardsService,
ConfigOption,
BoardDetails,
ConfigValue,
Programmer,
isBoardIdentifierChangeEvent,
isProgrammer,
sanitizeFqbn,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import type {
StartupTask,
StartupTaskProvider,
} from '../../electron-common/startup-task';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
export interface SelectConfigOptionParams {
readonly fqbn: string;
readonly optionsToUpdate: readonly Readonly<{
option: string;
selectedValue: string;
}>[];
}
@injectable()
export class BoardsDataStore implements FrontendApplicationContribution {
export class BoardsDataStore
implements
FrontendApplicationContribution,
StartupTaskProvider,
CommandContribution
{
@inject(ILogger)
@named('store')
protected readonly logger: ILogger;
private readonly logger: ILogger;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
private readonly boardsService: BoardsService;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
// When `@theia/workspace` is part of the application, the workspace-scoped storage service is the default implementation, and the `StorageService` symbol must be used for the injection.
// https://github.com/eclipse-theia/theia/blob/ba3722b04ff91eb6a4af6a571c9e263c77cdd8b5/packages/workspace/src/browser/workspace-frontend-module.ts#L97-L98
// In other words, store the data (such as the board configs) per sketch, not per IDE2 installation. https://github.com/arduino/arduino-ide/issues/2240
@inject(StorageService)
private readonly storageService: StorageService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<string[]>();
private readonly onDidChangeEmitter =
new Emitter<BoardsDataStoreChangeEvent>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeEmitter
);
private _selectedBoardData: BoardsDataStoreChange | undefined;
onStart(): void {
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
const dataDidChangePerFqbn: string[] = [];
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty)
.filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<
ConfigOption[] | undefined
>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateSelectedBoardData(
event.selectedBoard?.fqbn,
// If the change event comes from toolbar and the FQBN contains custom board options, change the currently selected options
// https://github.com/arduino/arduino-ide/issues/1588
event.reason === 'toolbar'
);
}
}),
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
const boardsWithFqbn = item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty);
const changes: BoardsDataStoreChange[] = [];
for (const fqbn of boardsWithFqbn) {
const key = this.getStorageKey(fqbn);
const storedData =
await this.storageService.getData<BoardsDataStore.Data>(key);
if (!storedData) {
// if no previously value is available for the board, do not update the cache
continue;
}
const details = await this.loadBoardDetails(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
dataDidChangePerFqbn.push(fqbn);
}
const data = createDataStoreEntry(details);
await this.storageService.setData(key, data);
changes.push({ fqbn, data });
}
}
if (changes.length) {
this.fireChanged(...changes);
}
}),
this.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (event.changes.find((change) => change.fqbn === selectedFqbn)) {
this.updateSelectedBoardData(selectedFqbn);
}
}),
]);
Promise.all([
this.boardsServiceProvider.ready,
this.appStateService.reachedState('ready'),
]).then(() =>
this.updateSelectedBoardData(
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
)
);
}
private async getSelectedBoardData(
fqbn: string | undefined
): Promise<BoardsDataStoreChange | undefined> {
if (!fqbn) {
return undefined;
} else {
const data = await this.getData(sanitizeFqbn(fqbn));
if (data === BoardsDataStore.Data.EMPTY) {
return undefined;
}
if (dataDidChangePerFqbn.length) {
this.fireChanged(...dataDidChangePerFqbn);
return { fqbn, data };
}
}
private async updateSelectedBoardData(
fqbn: string | undefined,
updateConfigOptions = false
): Promise<void> {
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
if (fqbn && updateConfigOptions) {
const { options } = new FQBN(fqbn);
if (options) {
const optionsToUpdate = Object.entries(options).map(([key, value]) => ({
option: key,
selectedValue: value,
}));
const params = { fqbn, optionsToUpdate };
await this.selectConfigOption(params);
this._selectedBoardData = await this.getSelectedBoardData(fqbn); // reload the updated data
}
}
}
onStop(): void {
this.toDispose.dispose();
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(USE_INHERITED_DATA, {
execute: async (arg: unknown) => {
if (isBoardsDataStoreChange(arg)) {
await this.setData(arg);
this.fireChanged(arg);
}
},
});
}
get onChanged(): Event<string[]> {
return this.onChangedEmitter.event;
tasks(): StartupTask[] {
if (!this._selectedBoardData) {
return [];
}
return [
{
command: USE_INHERITED_DATA.id,
args: [this._selectedBoardData],
},
];
}
get onDidChange(): Event<BoardsDataStoreChangeEvent> {
return this.onDidChangeEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined,
fqbn: string | undefined
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(fqbn);
return ConfigOption.decorate(fqbn, configOptions);
return new FQBN(fqbn).withConfigOptions(...configOptions).toString();
}
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
@ -80,83 +205,106 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<
const storedData = await this.storageService.getData<
BoardsDataStore.Data | undefined
>(key, undefined);
if (BoardsDataStore.Data.is(data)) {
return data;
if (BoardsDataStore.Data.is(storedData)) {
return storedData;
}
const boardDetails = await this.getBoardDetailsSafe(fqbn);
const boardDetails = await this.loadBoardDetails(fqbn);
if (!boardDetails) {
return BoardsDataStore.Data.EMPTY;
}
data = {
configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
const data = createDataStoreEntry(boardDetails);
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { programmers } = data;
async reloadBoardData(fqbn: string | undefined): Promise<void> {
if (!fqbn) {
return;
}
const key = this.getStorageKey(fqbn);
const details = await this.loadBoardDetails(fqbn, true);
if (!details) {
return;
}
const data = createDataStoreEntry(details);
await this.storageService.setData(key, data);
this.fireChanged({ fqbn, data });
}
async selectProgrammer({
fqbn,
selectedProgrammer,
}: {
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
const sanitizedFQBN = sanitizeFqbn(fqbn);
const storedData = deepClone(await this.getData(sanitizedFQBN));
const { programmers } = storedData;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
}
await this.setData({
fqbn,
data: { ...data, selectedProgrammer },
});
this.fireChanged(fqbn);
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: { ...storedData, selectedProgrammer },
};
await this.setData(change);
this.fireChanged(change);
return true;
}
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string }
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
async selectConfigOption(params: SelectConfigOptionParams): Promise<boolean> {
const { fqbn, optionsToUpdate } = params;
if (!optionsToUpdate.length) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
const sanitizedFQBN = sanitizeFqbn(fqbn);
const mutableData = deepClone(await this.getData(sanitizedFQBN));
let didChange = false;
for (const { option, selectedValue } of optionsToUpdate) {
const { configOptions } = mutableData;
const configOption = configOptions.find((c) => c.option === option);
if (configOption) {
const configOptionValueIndex = configOption.values.findIndex(
(configOptionValue) => configOptionValue.value === selectedValue
);
if (configOptionValueIndex >= 0) {
// unselect all
configOption.values
.map((value) => value as Mutable<ConfigValue>)
.forEach((value) => (value.selected = false));
const mutableConfigValue: Mutable<ConfigValue> =
configOption.values[configOptionValueIndex];
// make the new value `selected`
mutableConfigValue.selected = true;
didChange = true;
}
}
}
if (!updated) {
if (!didChange) {
return false;
}
await this.setData({ fqbn, data });
this.fireChanged(fqbn);
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: mutableData,
};
await this.setData(change);
this.fireChanged(change);
return true;
}
protected async setData({
fqbn,
data,
}: {
fqbn: string;
data: BoardsDataStore.Data;
}): Promise<void> {
protected async setData(change: BoardsDataStoreChange): Promise<void> {
const { fqbn, data } = change;
const key = this.getStorageKey(fqbn);
return this.storageService.setData(key, data);
}
@ -165,11 +313,15 @@ export class BoardsDataStore implements FrontendApplicationContribution {
return `.arduinoIDE-configOptions-${fqbn}`;
}
protected async getBoardDetailsSafe(
fqbn: string
async loadBoardDetails(
fqbn: string,
forceRefresh = false
): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
const details = await this.boardsService.getBoardDetails({
fqbn,
forceRefresh,
});
return details;
} catch (err) {
if (
@ -190,8 +342,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
}
protected fireChanged(...fqbn: string[]): void {
this.onChangedEmitter.fire(fqbn);
protected fireChanged(...changes: BoardsDataStoreChange[]): void {
this.onDidChangeEmitter.fire({ changes });
}
}
@ -200,20 +352,86 @@ export namespace BoardsDataStore {
readonly configOptions: ConfigOption[];
readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer;
readonly defaultProgrammerId?: string;
}
export namespace Data {
export const EMPTY: Data = {
export const EMPTY: Data = deepFreeze({
configOptions: [],
programmers: [],
};
export function is(arg: any): arg is Data {
});
export function is(arg: unknown): arg is Data {
return (
!!arg &&
'configOptions' in arg &&
Array.isArray(arg['configOptions']) &&
'programmers' in arg &&
Array.isArray(arg['programmers'])
typeof arg === 'object' &&
arg !== null &&
Array.isArray((<Data>arg).configOptions) &&
Array.isArray((<Data>arg).programmers) &&
((<Data>arg).selectedProgrammer === undefined ||
isProgrammer((<Data>arg).selectedProgrammer)) &&
((<Data>arg).defaultProgrammerId === undefined ||
typeof (<Data>arg).defaultProgrammerId === 'string')
);
}
}
}
export function isEmptyData(data: BoardsDataStore.Data): boolean {
return (
Boolean(!data.configOptions.length) &&
Boolean(!data.programmers.length) &&
Boolean(!data.selectedProgrammer) &&
Boolean(!data.defaultProgrammerId)
);
}
export function findDefaultProgrammer(
programmers: readonly Programmer[],
defaultProgrammerId: string | undefined | BoardsDataStore.Data
): Programmer | undefined {
if (!defaultProgrammerId) {
return undefined;
}
const id =
typeof defaultProgrammerId === 'string'
? defaultProgrammerId
: defaultProgrammerId.defaultProgrammerId;
return programmers.find((p) => p.id === id);
}
function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data {
const configOptions = details.configOptions.slice();
const programmers = details.programmers.slice();
const { defaultProgrammerId } = details;
const selectedProgrammer = findDefaultProgrammer(
programmers,
defaultProgrammerId
);
const data = {
configOptions,
programmers,
...(selectedProgrammer ? { selectedProgrammer } : {}),
...(defaultProgrammerId ? { defaultProgrammerId } : {}),
};
return data;
}
export interface BoardsDataStoreChange {
readonly fqbn: string;
readonly data: BoardsDataStore.Data;
}
function isBoardsDataStoreChange(arg: unknown): arg is BoardsDataStoreChange {
return (
typeof arg === 'object' &&
arg !== null &&
typeof (<BoardsDataStoreChange>arg).fqbn === 'string' &&
BoardsDataStore.Data.is((<BoardsDataStoreChange>arg).data)
);
}
export interface BoardsDataStoreChangeEvent {
readonly changes: readonly BoardsDataStoreChange[];
}
const USE_INHERITED_DATA: Command = {
id: 'arduino-use-inherited-boards-data',
};

View File

@ -1,16 +1,21 @@
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar';
import { codicon } from '@theia/core/lib/browser/widgets/widget';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { OpenBoardsConfig } from '../contributions/open-boards-config';
import {
BoardsServiceProvider,
AvailableBoard,
} from './boards-service-provider';
import { nls } from '@theia/core/lib/common';
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import ReactDOM from '@theia/core/shared/react-dom';
import classNames from 'classnames';
import { BoardsConfig } from './boards-config';
import { boardIdentifierLabel, Port } from '../../common/protocol';
import { BoardListItemUI } from '../../common/protocol/board-list';
import { assertUnreachable } from '../../common/utils';
import type {
BoardListUI,
BoardsServiceProvider,
} from './boards-service-provider';
export interface BoardsDropDownListCoords {
readonly top: number;
@ -22,18 +27,18 @@ export interface BoardsDropDownListCoords {
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
readonly boardList: BoardListUI;
readonly openBoardsConfig: () => void;
readonly hide: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement;
export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
private dropdownElement: HTMLElement;
private listRef: React.RefObject<HTMLDivElement>;
constructor(props: BoardsDropDown.Props) {
super(props);
this.listRef = React.createRef();
let list = document.getElementById('boards-dropdown-container');
if (!list) {
@ -51,11 +56,14 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
}
override render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
return ReactDOM.createPortal(
this.renderBoardListItems(),
this.dropdownElement
);
}
protected renderNode(): React.ReactNode {
const { coords, items } = this.props;
private renderBoardListItems(): React.ReactNode {
const { coords, boardList } = this.props;
if (coords === 'hidden') {
return '';
}
@ -74,14 +82,12 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
tabIndex={0}
>
<div className="arduino-boards-dropdown-list--items-container">
{items
.map(({ name, port, selected, onClick }) => ({
boardLabel: name,
port,
selected,
onClick,
}))
.map(this.renderItem)}
{boardList.items.map((item, index) =>
this.renderBoardListItem({
item,
selected: index === boardList.selectedIndex,
})
)}
</div>
<div
key={footerLabel}
@ -95,31 +101,43 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
);
}
protected renderItem({
boardLabel,
port,
private readonly onDefaultAction = (item: BoardListItemUI): unknown => {
const { boardList, hide } = this.props;
const { type, params } = item.defaultAction;
hide();
switch (type) {
case 'select-boards-config': {
return boardList.select(params);
}
case 'edit-boards-config': {
return boardList.edit(params);
}
default:
return assertUnreachable(type);
}
};
private renderBoardListItem({
item,
selected,
onClick,
}: {
boardLabel: string;
port: Port;
selected?: boolean;
onClick: () => void;
item: BoardListItemUI;
selected: boolean;
}): React.ReactNode {
const protocolIcon = iconNameFromProtocol(port.protocol);
const { boardLabel, portLabel, portProtocol, tooltip } = item.labels;
const port = item.port;
const onKeyUp = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
onClick();
this.onDefaultAction(item);
}
};
return (
<div
key={`board-item--${boardLabel}-${port.address}`}
key={`board-item--${Port.keyOf(port)}`}
className={classNames('arduino-boards-dropdown-item', {
'arduino-boards-dropdown-item--selected': selected,
})}
onClick={onClick}
onClick={() => this.onDefaultAction(item)}
onKeyUp={onKeyUp}
tabIndex={0}
>
@ -127,21 +145,81 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
className={classNames(
'arduino-boards-dropdown-item--protocol',
'fa',
protocolIcon
iconNameFromProtocol(portProtocol)
)}
/>
<div
className="arduino-boards-dropdown-item--label"
title={`${boardLabel}\n${port.address}`}
>
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
{boardLabel}
<div className="arduino-boards-dropdown-item--label" title={tooltip}>
<div className="arduino-boards-dropdown-item--board-header">
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
{boardLabel}
</div>
</div>
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
{port.addressLabel}
{portLabel}
</div>
</div>
{selected ? <div className="fa fa-check" /> : ''}
{this.renderActions(item)}
</div>
);
}
private renderActions(item: BoardListItemUI): React.ReactNode {
const { boardList, hide } = this.props;
const { revert, edit } = item.otherActions;
if (!edit && !revert) {
return undefined;
}
const handleOnClick = (
event: React.MouseEvent<HTMLElement, MouseEvent>,
callback: () => void
) => {
event.preventDefault();
event.stopPropagation();
hide();
callback();
};
return (
<div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}>
{edit && (
<div
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
>
{
<div
id="edit"
className={codicon('pencil', true)}
title={nls.localize(
'arduino/board/editBoardsConfig',
'Edit Board and Port...'
)}
onClick={(event) =>
handleOnClick(event, () => boardList.edit(edit.params))
}
/>
}
</div>
)}
{revert && (
<div
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
>
{
<div
id="revert"
className={codicon('discard', true)}
title={nls.localize(
'arduino/board/revertBoardsConfig',
"Use '{0}' discovered on '{1}'",
boardIdentifierLabel(revert.params.selectedBoard),
item.labels.portLabel
)}
onClick={(event) =>
handleOnClick(event, () => boardList.select(revert.params))
}
/>
}
</div>
)}
</div>
);
}
@ -153,26 +231,27 @@ export class BoardsToolBarItem extends React.Component<
> {
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection =
new DisposableCollection();
private readonly toDispose: DisposableCollection;
constructor(props: BoardsToolBarItem.Props) {
super(props);
const { availableBoards } = props.boardsServiceProvider;
const { boardList } = props.boardsServiceProvider;
this.state = {
availableBoards,
boardList,
coords: 'hidden',
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
const listener = () => this.setState({ coords: 'hidden' });
document.addEventListener('click', listener);
this.toDispose = new DisposableCollection(
Disposable.create(() => document.removeEventListener('click', listener))
);
}
override componentDidMount(): void {
this.props.boardsServiceProvider.onAvailableBoardsChanged(
(availableBoards) => this.setState({ availableBoards })
this.toDispose.push(
this.props.boardsServiceProvider.onBoardListDidChange((boardList) =>
this.setState({ boardList })
)
);
}
@ -180,7 +259,7 @@ export class BoardsToolBarItem extends React.Component<
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
private readonly show = (event: React.MouseEvent<HTMLElement>): void => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
@ -201,31 +280,26 @@ export class BoardsToolBarItem extends React.Component<
event.nativeEvent.stopImmediatePropagation();
};
private readonly hide = () => {
this.setState({ coords: 'hidden' });
};
override render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const { selectedBoard, selectedPort } =
this.props.boardsServiceProvider.boardsConfig;
const boardLabel =
selectedBoard?.name ||
nls.localize('arduino/board/selectBoard', 'Select Board');
const selectedPortLabel = portLabel(selectedPort?.address);
const isConnected = Boolean(selectedBoard && selectedPort);
const protocolIcon = isConnected
? iconNameFromProtocol(selectedPort?.protocol || '')
const { coords, boardList } = this.state;
const { boardLabel, selected, portProtocol, tooltip } = boardList.labels;
const protocolIcon = portProtocol
? iconNameFromProtocol(portProtocol)
: null;
const protocolIconClassNames = classNames(
'arduino-boards-toolbar-item--protocol',
'fa',
protocolIcon
);
return (
<React.Fragment>
<div
className="arduino-boards-toolbar-item-container"
title={selectedPortLabel}
title={tooltip}
onClick={this.show}
>
{protocolIcon && <div className={protocolIconClassNames} />}
@ -234,57 +308,22 @@ export class BoardsToolBarItem extends React.Component<
'arduino-boards-toolbar-item--label',
'noWrapInfo',
'noselect',
{ 'arduino-boards-toolbar-item--label-connected': isConnected }
{ 'arduino-boards-toolbar-item--label-connected': selected }
)}
>
{boardLabel}
</div>
<div className="fa fa-caret-down caret" />
</div>
<BoardsDropDown
<BoardListDropDown
coords={coords}
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (!board.fqbn) {
const previousBoardConfig =
this.props.boardsServiceProvider.boardsConfig;
this.props.boardsServiceProvider.boardsConfig = {
selectedPort: board.port,
};
this.openDialog(previousBoardConfig);
} else {
this.props.boardsServiceProvider.boardsConfig = {
selectedBoard: board,
selectedPort: board.port,
};
}
this.setState({ coords: 'hidden' });
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
boardList={boardList}
openBoardsConfig={() => boardList.edit({ query: '' })}
hide={this.hide}
/>
</React.Fragment>
);
}
protected openDialog = async (
previousBoardConfig?: BoardsConfig.Config
): Promise<void> => {
const selectedBoardConfig =
await this.props.commands.executeCommand<BoardsConfig.Config>(
OpenBoardsConfig.Commands.OPEN_DIALOG.id
);
if (
previousBoardConfig &&
(!selectedBoardConfig?.selectedPort ||
!selectedBoardConfig?.selectedBoard)
) {
this.props.boardsServiceProvider.boardsConfig = previousBoardConfig;
}
};
}
export namespace BoardsToolBarItem {
export interface Props {
@ -293,7 +332,7 @@ export namespace BoardsToolBarItem {
}
export interface State {
availableBoards: AvailableBoard[];
boardList: BoardListUI;
coords: BoardsDropDownListCoords | 'hidden';
}
}
@ -304,19 +343,10 @@ function iconNameFromProtocol(protocol: string): string {
return 'fa-arduino-technology-usb';
case 'network':
return 'fa-arduino-technology-connection';
/*
Bluetooth ports are not listed yet from the CLI;
Not sure about the naming ('bluetooth'); make sure it's correct before uncommenting the following lines
*/
// case 'bluetooth':
// return 'fa-arduino-technology-bluetooth';
// it is fine to assign dedicated icons to the protocols used by the official boards,
// but other than that it is best to avoid implementing any special handling
// for specific protocols in the IDE codebase.
default:
return 'fa-arduino-technology-3dimensionscube';
}
}
function portLabel(portName?: string): string {
return portName
? nls.localize('arduino/board/portLabel', 'Port: {0}', portName)
: nls.localize('arduino/board/disconnected', 'Disconnected');
}

View File

@ -1,4 +1,4 @@
import * as React from '@theia/core/shared/react';
import React from '@theia/core/shared/react';
export type ProgressBarProps = {
percent?: number;

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,123 @@
import type { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
BoardDetails,
Programmer,
isBoardIdentifierChangeEvent,
} from '../../common/protocol';
import {
BoardsDataStore,
findDefaultProgrammer,
isEmptyData,
} from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution } from './contribution';
/**
* Before CLI 0.35.0-rc.3, there was no `programmer#default` property in the `board details` response.
* This method does the programmer migration in the data store. If there is a programmer selected, it's a noop.
* If no programmer is selected, it forcefully reloads the details from the CLI and updates it in the local storage.
*/
@injectable()
export class AutoSelectProgrammer extends Contribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
override onStart(): void {
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.ensureProgrammerIsSelected();
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() =>
this.ensureProgrammerIsSelected()
);
}
private async ensureProgrammerIsSelected(): Promise<boolean> {
return ensureProgrammerIsSelected({
fqbn: this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
getData: (fqbn) => this.boardsDataStore.getData(fqbn),
loadBoardDetails: (fqbn) => this.boardsDataStore.loadBoardDetails(fqbn),
selectProgrammer: (arg) => this.boardsDataStore.selectProgrammer(arg),
});
}
}
interface EnsureProgrammerIsSelectedParams {
fqbn: string | undefined;
getData: (fqbn: string | undefined) => MaybePromise<BoardsDataStore.Data>;
loadBoardDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>;
selectProgrammer(options: {
fqbn: string;
selectedProgrammer: Programmer;
}): MaybePromise<boolean>;
}
export async function ensureProgrammerIsSelected(
params: EnsureProgrammerIsSelectedParams
): Promise<boolean> {
const { fqbn, getData, loadBoardDetails, selectProgrammer } = params;
if (!fqbn) {
return false;
}
console.debug(`Ensuring a programmer is selected for ${fqbn}...`);
const data = await getData(fqbn);
if (isEmptyData(data)) {
// For example, the platform is not installed.
console.debug(`Skipping. No boards data is available for ${fqbn}.`);
return false;
}
if (data.selectedProgrammer) {
console.debug(
`A programmer is already selected for ${fqbn}: '${data.selectedProgrammer.id}'.`
);
return true;
}
let programmer = findDefaultProgrammer(data.programmers, data);
if (programmer) {
// select the programmer if the default info is available
const result = await selectProgrammer({
fqbn,
selectedProgrammer: programmer,
});
if (result) {
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
return result;
}
}
console.debug(`Reloading board details for ${fqbn}...`);
const reloadedData = await loadBoardDetails(fqbn);
if (!reloadedData) {
console.debug(`Skipping. No board details found for ${fqbn}.`);
return false;
}
if (!reloadedData.programmers.length) {
console.debug(`Skipping. ${fqbn} does not have programmers.`);
return false;
}
programmer = findDefaultProgrammer(reloadedData.programmers, reloadedData);
if (!programmer) {
console.debug(
`Skipping. Could not find a default programmer for ${fqbn}. Programmers were: `
);
return false;
}
const result = await selectProgrammer({
fqbn,
selectedProgrammer: programmer,
});
if (result) {
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
} else {
console.debug(
`Could not select '${programmer.id}' programmer for ${fqbn}.`
);
}
return result;
}

View File

@ -1,58 +1,61 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
DisposableCollection,
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { BoardsConfig } from '../boards/boards-config';
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import type { MenuPath } from '@theia/core/lib/common/menu/menu-types';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MainMenuManager } from '../../common/main-menu-manager';
import {
BoardsService,
BoardWithPackage,
createPlatformIdentifier,
getBoardInfo,
InstalledBoardWithPackage,
platformIdentifierEquals,
Port,
serializePlatformIdentifier,
} from '../../common/protocol';
import type { BoardList } from '../../common/protocol/board-list';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
ArduinoMenus,
PlaceholderMenuNode,
unregisterSubmenu,
} from '../menu/arduino-menus';
import {
BoardsService,
InstalledBoardWithPackage,
AvailablePorts,
Port,
getBoardInfo,
} from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { nls } from '@theia/core/lib/common';
import { NotificationCenter } from '../notification-center';
import { Command, CommandRegistry, SketchContribution } from './contribution';
@injectable()
export class BoardSelection extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
private readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry;
private readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
private readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
// do not query installed platforms on every change
private _installedBoards: Deferred<InstalledBoardWithPackage[]> | undefined;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const boardInfo = await getBoardInfo(
this.boardsServiceProvider.boardsConfig.selectedPort,
this.boardsService.getState()
this.boardsServiceProvider.boardList
);
if (typeof boardInfo === 'string') {
this.messageService.info(boardInfo);
@ -65,7 +68,7 @@ VID: ${VID}
PID: ${PID}
SN: ${SN}
`.trim();
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
await this.dialogService.showMessageBox({
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
@ -74,37 +77,61 @@ SN: ${SN}
});
},
});
registry.registerCommand(BoardSelection.Commands.RELOAD_BOARD_DATA, {
execute: async () => {
const selectedFqbn =
this.boardsServiceProvider.boardList.boardsConfig.selectedBoard?.fqbn;
let message: string;
if (selectedFqbn) {
await this.boardsDataStore.reloadBoardData(selectedFqbn);
message = nls.localize(
'arduino/board/boardDataReloaded',
'Board data reloaded.'
);
} else {
message = nls.localize(
'arduino/board/selectBoardToReload',
'Please select a board first.'
);
}
this.messageService.info(message, { timeout: 2000 });
},
});
}
override onStart(): void {
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus());
this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus());
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onAvailablePortsChanged(() =>
this.updateMenus()
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus(true));
this.notificationCenter.onPlatformDidUninstall(() =>
this.updateMenus(true)
);
this.boardsServiceProvider.onBoardListDidChange(() => this.updateMenus());
}
override async onReady(): Promise<void> {
this.updateMenus();
}
protected async updateMenus(): Promise<void> {
const [installedBoards, availablePorts, config] = await Promise.all([
this.installedBoards(),
this.boardsService.getState(),
this.boardsServiceProvider.boardsConfig,
]);
this.rebuildMenus(installedBoards, availablePorts, config);
private async updateMenus(discardCache = false): Promise<void> {
if (discardCache) {
this._installedBoards?.reject();
this._installedBoards = undefined;
}
if (!this._installedBoards) {
this._installedBoards = new Deferred();
this.installedBoards().then((installedBoards) =>
this._installedBoards?.resolve(installedBoards)
);
}
const installedBoards = await this._installedBoards.promise;
this.rebuildMenus(installedBoards, this.boardsServiceProvider.boardList);
}
protected rebuildMenus(
private rebuildMenus(
installedBoards: InstalledBoardWithPackage[],
availablePorts: AvailablePorts,
config: BoardsConfig.Config
boardList: BoardList
): void {
this.toDisposeBeforeMenuRebuild.dispose();
@ -113,7 +140,8 @@ SN: ${SN}
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'1_boards',
];
const boardsSubmenuLabel = config.selectedBoard?.name;
const { selectedBoard, selectedPort } = boardList.boardsConfig;
const boardsSubmenuLabel = selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
@ -133,7 +161,7 @@ SN: ${SN}
// Ports submenu
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
const portsSubmenuLabel = config.selectedPort?.address;
const portsSubmenuLabel = selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
nls.localize(
@ -149,6 +177,21 @@ SN: ${SN}
)
);
const reloadBoardData = {
commandId: BoardSelection.Commands.RELOAD_BOARD_DATA.id,
label: nls.localize('arduino/board/reloadBoardData', 'Reload Board Data'),
order: '102',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
reloadBoardData
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(reloadBoardData)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
@ -172,69 +215,116 @@ SN: ${SN}
label: `${BoardsListWidget.WIDGET_LABEL}...`,
});
const selectedBoardPlatformId = selectedBoard
? createPlatformIdentifier(selectedBoard)
: undefined;
// Keys are the vendor IDs
type BoardsPerVendor = Record<string, BoardWithPackage[]>;
// Group boards by their platform names. The keys are the platform names as menu labels.
// If there is a platform name (menu label) collision, refine the menu label with the vendor ID.
const groupedBoards = new Map<string, BoardsPerVendor>();
for (const board of installedBoards) {
const { packageId, packageName } = board;
const { vendorId } = packageId;
let boardsPerPackageName = groupedBoards.get(packageName);
if (!boardsPerPackageName) {
boardsPerPackageName = {} as BoardsPerVendor;
groupedBoards.set(packageName, boardsPerPackageName);
}
let boardPerVendor: BoardWithPackage[] | undefined =
boardsPerPackageName[vendorId];
if (!boardPerVendor) {
boardPerVendor = [];
boardsPerPackageName[vendorId] = boardPerVendor;
}
boardPerVendor.push(board);
}
// Installed boards
installedBoards.forEach((board, index) => {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
Array.from(groupedBoards.entries()).forEach(
([packageName, boardsPerPackage]) => {
const useVendorSuffix = Object.keys(boardsPerPackage).length > 1;
Object.entries(boardsPerPackage).forEach(([vendorId, boards]) => {
let platformMenuPath: MenuPath | undefined = undefined;
boards.forEach((board, index) => {
const { packageId, fqbn, name, manuallyInstalled } = board;
// create the platform submenu once.
// creating and registering the same submenu twice in Theia is a noop, though.
if (!platformMenuPath) {
let packageLabel =
packageName +
`${
manuallyInstalled
? nls.localize(
'arduino/board/inSketchbook',
' (in Sketchbook)'
)
: ''
}`;
if (
selectedBoardPlatformId &&
platformIdentifierEquals(packageId, selectedBoardPlatformId)
) {
packageLabel = `${packageLabel}`;
}
if (useVendorSuffix) {
packageLabel += ` (${vendorId})`;
}
// Platform submenu
platformMenuPath = [
...boardsPackagesGroup,
serializePlatformIdentifier(packageId),
];
this.menuModelRegistry.registerSubmenu(
platformMenuPath,
packageLabel,
{
order: packageName.toLowerCase(),
}
);
}
const packageLabel =
packageName +
`${
manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
}`;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, {
order: packageName.toLowerCase(),
});
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () => {
if (
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: {
name,
fqbn,
port: this.boardsServiceProvider.boardsConfig.selectedBoard
?.port, // TODO: verify!
},
selectedPort:
this.boardsServiceProvider.boardsConfig.selectedPort,
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () =>
this.boardsServiceProvider.updateConfig({
name: name,
fqbn: fqbn,
}),
isToggled: () => fqbn === selectedBoard?.fqbn,
};
}
},
isToggled: () =>
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
};
// Board menu
const menuAction = {
commandId: id,
label: name,
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
});
// Board menu
const menuAction = {
commandId: id,
label: name,
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(
platformMenuPath,
menuAction
);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
});
});
}
);
// Installed ports
// Detected ports
const registerPorts = (
protocol: string,
protocolOrder: number,
ports: AvailablePorts
ports: ReturnType<BoardList['ports']>,
protocolOrder: number
) => {
const portIDs = Object.keys(ports);
if (!portIDs.length) {
if (!ports.length) {
return;
}
@ -259,46 +349,26 @@ SN: ${SN}
)
);
// First we show addresses with recognized boards connected,
// then all the rest.
const sortedIDs = Object.keys(ports).sort(
(left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
}
);
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
const [port, boards] = ports[portID];
for (let i = 0; i < ports.length; i++) {
const { port, boards } = ports[i];
const portKey = Port.keyOf(port);
let label = `${port.addressLabel}`;
if (boards.length) {
if (boards?.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
}
const id = `arduino-select-port--${portID}`;
const id = `arduino-select-port--${portKey}`;
const command = { id };
const handler = {
execute: () => {
if (
!Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port,
};
}
this.boardsServiceProvider.updateConfig({
protocol: port.protocol,
address: port.address,
});
},
isToggled: () => {
return i === ports.matchingIndex;
},
isToggled: () =>
Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
),
};
const menuAction = {
commandId: id,
@ -315,22 +385,12 @@ SN: ${SN}
}
};
const grouped = AvailablePorts.groupByProtocol(availablePorts);
const groupedPorts = boardList.portsGroupedByProtocol();
let protocolOrder = 100;
// We first show serial and network ports, then all the rest
['serial', 'network'].forEach((protocol) => {
const ports = grouped.get(protocol);
if (ports) {
registerPorts(protocol, protocolOrder, ports);
grouped.delete(protocol);
protocolOrder = protocolOrder + 100;
}
Object.entries(groupedPorts).forEach(([protocol, ports]) => {
registerPorts(protocol, ports, protocolOrder);
protocolOrder += 100;
});
grouped.forEach((ports, protocol) => {
registerPorts(protocol, protocolOrder, ports);
protocolOrder = protocolOrder + 100;
});
this.mainMenuManager.update();
}
@ -342,5 +402,8 @@ SN: ${SN}
export namespace BoardSelection {
export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
export const RELOAD_BOARD_DATA: Command = {
id: 'arduino-reload-board-data',
};
}
}

View File

@ -1,67 +1,66 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { BoardsServiceProvider } from './boards-service-provider';
import { Board, ConfigOption, Programmer } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import PQueue from 'p-queue';
import {
BoardIdentifier,
ConfigOption,
isBoardIdentifierChangeEvent,
Programmer,
} from '../../common/protocol';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
CommandRegistry,
Contribution,
MenuModelRegistry,
} from './contribution';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
export class BoardsDataMenuUpdater extends Contribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
private readonly menuRegistry: MenuModelRegistry;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
private readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
private readonly toDisposeOnBoardChange = new DisposableCollection();
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.appStateService
.reachedState('ready')
.then(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsDataStore.onChanged(() =>
override onStart(): void {
this.boardsDataStore.onDidChange(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
this.boardsServiceProvider.boardsConfig.selectedBoard
)
);
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.updateMenuActions(selectedBoard)
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateMenuActions(event.selectedBoard);
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() =>
this.updateMenuActions(
this.boardsServiceProvider.boardsConfig.selectedBoard
)
);
}
protected async updateMenuActions(
selectedBoard: Board | undefined
private async updateMenuActions(
selectedBoard: BoardIdentifier | undefined
): Promise<void> {
return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update();
this.menuManager.update();
if (selectedBoard) {
const { fqbn } = selectedBoard;
if (fqbn) {
@ -88,8 +87,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
execute: () =>
this.boardsDataStore.selectConfigOption({
fqbn,
option,
selectedValue: value.value,
optionsToUpdate: [{ option, selectedValue: value.value }],
}),
isToggled: () => value.selected,
};
@ -172,7 +170,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
]);
}
}
this.mainMenuManager.update();
this.menuManager.update();
}
}
});

View File

@ -37,11 +37,15 @@ export class BurnBootloader extends CoreServiceContribution {
'arduino/bootloader/burningBootloader',
'Burning bootloader...'
),
task: (progressId, coreService) =>
coreService.burnBootloader({
...options,
progressId,
}),
task: (progressId, coreService, token) =>
coreService.burnBootloader(
{
...options,
progressId,
},
token
),
cancelable: true,
});
this.messageService.info(
nls.localize(

View File

@ -3,10 +3,14 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
IDEUpdater,
LAST_USED_IDE_VERSION,
SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution';
import { VersionWelcomeDialog } from '../dialogs/version-welcome-dialog';
import { AppService } from '../app-service';
import { SemVer } from 'semver';
@injectable()
export class CheckForIDEUpdates extends Contribution {
@ -16,9 +20,15 @@ export class CheckForIDEUpdates extends Contribution {
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(VersionWelcomeDialog)
private readonly versionWelcomeDialog: VersionWelcomeDialog;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
@inject(AppService)
private readonly appService: AppService;
override onStart(): void {
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
@ -36,7 +46,7 @@ export class CheckForIDEUpdates extends Contribution {
);
}
override onReady(): void {
override async onReady(): Promise<void> {
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
@ -49,12 +59,18 @@ export class CheckForIDEUpdates extends Contribution {
return this.updater.checkForUpdates(true);
})
.then(async (updateInfo) => {
if (!updateInfo) return;
if (!updateInfo) {
const isNewVersion = await this.isNewStableVersion();
if (isNewVersion) {
this.versionWelcomeDialog.open();
}
return;
}
const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
this.updaterDialog.open(true, updateInfo);
})
.catch((e) => {
this.messageService.error(
@ -64,6 +80,44 @@ export class CheckForIDEUpdates extends Contribution {
e.message
)
);
})
.finally(() => {
this.setCurrentIDEVersion();
});
}
private async setCurrentIDEVersion(): Promise<void> {
try {
const { appVersion } = await this.appService.info();
const currSemVer = new SemVer(appVersion ?? '');
this.localStorage.setData(LAST_USED_IDE_VERSION, currSemVer.format());
} catch {
// ignore invalid versions
}
}
/**
* Check if user is running a new IDE version for the first time.
* @returns true if the current IDE version is greater than the last used version
* and both are non-prerelease versions.
*/
private async isNewStableVersion(): Promise<boolean> {
try {
const { appVersion } = await this.appService.info();
const prevVersion = await this.localStorage.getData<string>(
LAST_USED_IDE_VERSION
);
const prevSemVer = new SemVer(prevVersion ?? '');
const currSemVer = new SemVer(appVersion ?? '');
if (prevSemVer.prerelease.length || currSemVer.prerelease.length) {
return false;
}
return currSemVer.compare(prevSemVer) === 1;
} catch (e) {
return false;
}
}
}

View File

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

View File

@ -779,7 +779,7 @@ export class CompilerErrors
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.getByUri(new URI(uriOrWidget.toString()))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);

View File

@ -1,81 +1,87 @@
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
KeybindingContribution,
KeybindingRegistry,
} from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ILogger } from '@theia/core/lib/common/logger';
import {
MenuContribution,
MenuModelRegistry,
} from '@theia/core/lib/common/menu';
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 { MaybePromise, isObject } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
interfaces,
postConstruct,
} 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';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
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 { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { OutputChannelSeverity } from '@theia/output/lib/browser/output-channel';
import { MainMenuManager } from '../../common/main-menu-manager';
import { userAbort } from '../../common/nls';
import {
MenuModelRegistry,
MenuContribution,
} from '@theia/core/lib/common/menu';
CoreError,
CoreService,
FileSystemExt,
ResponseServiceClient,
Sketch,
SketchesService,
} from '../../common/protocol';
import {
KeybindingRegistry,
KeybindingContribution,
} from '@theia/core/lib/browser/keybinding';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
FrontendApplicationContribution,
FrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import {
Command,
CommandRegistry,
CommandContribution,
CommandService,
} from '@theia/core/lib/common/command';
ExecuteWithProgress,
UserAbortApplicationError,
} from '../../common/protocol/progressible';
import { ArduinoPreferences } from '../arduino-preferences';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ConfigServiceClient } from '../config/config-service-client';
import { DialogService } from '../dialog-service';
import { SettingsService } from '../dialogs/settings/settings';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../sketches-service-client-impl';
import {
SketchesService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
ResponseServiceClient,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { OutputChannelManager } from '../theia/output/output-channel';
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/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,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
TabBarToolbarRegistry,
URI,
Sketch,
open,
};
@ -115,6 +121,9 @@ export abstract class Contribution
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(DialogService)
protected readonly dialogService: DialogService;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
@ -168,6 +177,9 @@ export abstract class SketchContribution extends Contribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ApplicationConnectionStatusContribution)
protected readonly connectionStatusService: ApplicationConnectionStatusContribution;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
@ -239,6 +251,12 @@ export abstract class CoreServiceContribution extends SketchContribution {
}
protected handleError(error: unknown): void {
if (isObject(error) && UserAbortApplicationError.is(error)) {
this.outputChannelManager
.getChannel('Arduino')
.appendLine(userAbort, OutputChannelSeverity.Warning);
return;
}
this.tryToastErrorMessage(error);
}
@ -285,7 +303,13 @@ export abstract class CoreServiceContribution extends SketchContribution {
protected async doWithProgress<T>(options: {
progressText: string;
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
task: (
progressId: string,
coreService: CoreService,
cancellationToken?: CancellationToken
) => Promise<T>;
// false by default
cancelable?: boolean;
}): Promise<T> {
const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget()
@ -298,8 +322,10 @@ export abstract class CoreServiceContribution extends SketchContribution {
messageService: this.messageService,
responseService: this.responseService,
progressText,
run: ({ progressId }) => task(progressId, this.coreService),
run: ({ progressId, cancellationToken }) =>
task(progressId, this.coreService, cancellationToken),
keepOutput,
cancelable: options.cancelable,
});
toDisposeOnComplete.dispose();
return result;

View File

@ -1,102 +1,173 @@
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import { nls } from '@theia/core/lib/common/nls';
import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { NotificationCenter } from '../notification-center';
import { noBoardSelected } from '../../common/nls';
import {
Board,
BoardDetails,
BoardIdentifier,
BoardsService,
CheckDebugEnabledParams,
ExecutableService,
Sketch,
SketchRef,
isBoardIdentifierChangeEvent,
isCompileSummary,
} from '../../common/protocol';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import { ArduinoMenus } from '../menu/arduino-menus';
import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
URI,
Command,
CommandRegistry,
SketchContribution,
TabBarToolbarRegistry,
URI,
} from './contribution';
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
interface StartDebugParams {
/**
* Absolute filesystem path to the Arduino CLI executable.
*/
readonly cliPath: string;
/**
* The the board to debug.
*/
readonly board: Readonly<{ fqbn: string; name?: string }>;
/**
* Absolute filesystem path of the sketch to debug.
*/
readonly sketchPath: string;
/**
* Location where the `launch.json` will be created on the fly before starting every debug session.
* If not defined, it falls back to `sketchPath/.vscode/launch.json`.
*/
readonly launchConfigsDirPath?: string;
/**
* Absolute path to the `arduino-cli.yaml` file. If not specified, it falls back to `~/.arduinoIDE/arduino-cli.yaml`.
*/
readonly cliConfigPath?: string;
/**
* Programmer for the debugging.
*/
readonly programmer?: string;
/**
* Custom progress title to use when getting the debug information from the CLI.
*/
readonly title?: string;
}
type StartDebugResult = boolean;
export const DebugDisabledStatusMessageSource = Symbol(
'DebugDisabledStatusMessageSource'
);
export interface DebugDisabledStatusMessageSource {
/**
* `undefined` if debugging is enabled (for the currently selected board + programmer + config options).
* Otherwise, it's the human readable message why it's disabled.
*/
get message(): string | undefined;
/**
* Emits an event when {@link message} changes.
*/
get onDidChangeMessage(): Event<string | undefined>;
}
@injectable()
export class Debug extends SketchContribution {
export class Debug
extends SketchContribution
implements DebugDisabledStatusMessageSource
{
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(ExecutableService)
private readonly executableService: ExecutableService;
@inject(BoardsService)
private readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
* If `undefined`, debugging is enabled. Otherwise, the human-readable reason why it's disabled.
*/
private _disabledMessages?: string = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
); // Initial pessimism.
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
private onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
private _message?: string = noBoardSelected; // Initial pessimism.
private readonly didChangeMessageEmitter = new Emitter<string | undefined>();
readonly onDidChangeMessage = this.didChangeMessageEmitter.event;
private get disabledMessage(): string | undefined {
return this._disabledMessages;
get message(): string | undefined {
return this._message;
}
private set disabledMessage(message: string | undefined) {
this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
private set message(message: string | undefined) {
this._message = message;
this.didChangeMessageEmitter.fire(this._message);
}
private readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${
this.disabledMessage
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`,
priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>,
onDidChange: this.onDidChangeMessage as Event<void>,
};
override onStart(): void {
this.onDisabledMessageDidChange(
this.onDidChangeMessage(
() =>
(this.debugToolbarItem.tooltip = `${
this.disabledMessage
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`)
);
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateMessage();
}
});
this.notificationCenter.onPlatformDidInstall(() => this.updateMessage());
this.notificationCenter.onPlatformDidUninstall(() => this.updateMessage());
this.boardsDataStore.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (event.changes.find((change) => change.fqbn === selectedFqbn)) {
this.updateMessage();
}
});
this.commandService.onDidExecuteCommand((event) => {
const { commandId, args } = event;
if (
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
isCompileSummary(args[0])
) {
this.updateMessage();
}
});
}
override onReady(): MaybePromise<void> {
this.refreshState();
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.updateMessage());
}
override registerCommands(registry: CommandRegistry): void {
@ -104,7 +175,7 @@ export class Debug extends SketchContribution {
execute: () => this.startDebug(),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage,
isEnabled: () => !this.message,
});
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
execute: () => this.toggleCompileForDebug(),
@ -127,94 +198,56 @@ export class Debug extends SketchContribution {
});
}
private async refreshState(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
private async updateMessage(): Promise<void> {
try {
await this.isDebugEnabled();
this.message = undefined;
} catch (err) {
let message = String(err);
if (err instanceof Error) {
message = err.message;
}
this.message = message;
}
}
private async startDebug(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
private async isDebugEnabled(
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
return;
): Promise<string> {
const debugFqbn = await isDebugEnabled(
board,
(fqbn) => this.boardService.getBoardDetails({ fqbn }),
(fqbn) => this.boardsDataStore.getData(fqbn),
(fqbn) => this.boardsDataStore.appendConfigToFqbn(fqbn),
(params) => this.boardService.checkDebugEnabled(params)
);
return debugFqbn;
}
private async startDebug(
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard,
sketch:
| CurrentSketch
| undefined = this.sketchServiceClient.tryGetCurrentSketch()
): Promise<StartDebugResult> {
if (!CurrentSketch.isValid(sketch)) {
return false;
}
const { name, fqbn } = board;
if (!fqbn) {
return;
const params = await this.createStartDebugParams(board);
if (!params) {
return false;
}
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
try {
await this.commandService.executeCommand('arduino.debug.start', config);
const result = await this.debug(params);
return Boolean(result);
} catch (err) {
if (await this.isSketchNotVerifiedError(err, sketch)) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const answer = await this.messageService.error(
nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketch.name
),
sketchIsNotCompiled(sketch.name),
yes
);
if (answer === yes) {
@ -226,6 +259,16 @@ export class Debug extends SketchContribution {
);
}
}
return false;
}
private async debug(
params: StartDebugParams
): Promise<StartDebugResult | undefined> {
return this.commandService.executeCommand<StartDebugResult>(
'arduino.debug.start',
params
);
}
get compileForDebug(): boolean {
@ -233,7 +276,7 @@ export class Debug extends SketchContribution {
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> {
private toggleCompileForDebug(): void {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
@ -242,12 +285,12 @@ export class Debug extends SketchContribution {
private async isSketchNotVerifiedError(
err: unknown,
sketch: Sketch
sketch: SketchRef
): Promise<boolean> {
if (err instanceof Error) {
try {
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return tempBuildPaths.some((tempBuildPath) =>
const buildPaths = await this.sketchesService.getBuildPath(sketch);
return buildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
@ -256,6 +299,48 @@ export class Debug extends SketchContribution {
}
return false;
}
private async createStartDebugParams(
board: BoardIdentifier | undefined
): Promise<StartDebugParams | undefined> {
if (!board || !board.fqbn) {
return undefined;
}
let debugFqbn: string | undefined = undefined;
try {
debugFqbn = await this.isDebugEnabled(board);
} catch {}
if (!debugFqbn) {
return undefined;
}
const [sketch, executables, boardsData] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
this.boardsDataStore.getData(board.fqbn),
]);
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, launchConfigsDirPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
return {
board: { fqbn: debugFqbn, name: board.name },
cliPath,
sketchPath,
launchConfigsDirPath,
programmer: boardsData.selectedProgrammer?.id,
title: nls.localize(
'arduino/debug/getDebugInfo',
'Getting debug info...'
),
};
}
}
export namespace Debug {
export namespace Commands {
@ -280,3 +365,78 @@ export namespace Debug {
};
}
}
/**
* Resolves with the FQBN to use for the `debug --info --programmer p --fqbn $FQBN` command. Otherwise, rejects.
*
* (non-API)
*/
export async function isDebugEnabled(
board: BoardIdentifier | undefined,
getDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>,
getData: (fqbn: string) => MaybePromise<BoardsDataStore.Data>,
appendConfigToFqbn: (fqbn: string) => MaybePromise<string | undefined>,
checkDebugEnabled: (params: CheckDebugEnabledParams) => MaybePromise<string>
): Promise<string> {
if (!board) {
throw new Error(noBoardSelected);
}
const { fqbn } = board;
if (!fqbn) {
throw new Error(noPlatformInstalledFor(board.name));
}
const [details, data, fqbnWithConfig] = await Promise.all([
getDetails(fqbn),
getData(fqbn),
appendConfigToFqbn(fqbn),
]);
if (!details) {
throw new Error(noPlatformInstalledFor(board.name));
}
if (!fqbnWithConfig) {
throw new Error(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
const params = {
fqbn: fqbnWithConfig,
programmer: data.selectedProgrammer?.id,
};
try {
const debugFqbn = await checkDebugEnabled(params);
return debugFqbn;
} catch (err) {
throw new Error(debuggingNotSupported(board.name));
}
}
/**
* (non-API)
*/
export function sketchIsNotCompiled(sketchName: string): string {
return nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketchName
);
}
/**
* (non-API)
*/
export function noPlatformInstalledFor(boardName: string): string {
return nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
boardName
);
}
/**
* (non-API)
*/
export function debuggingNotSupported(boardName: string): string {
return nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
boardName
);
}

View File

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

View File

@ -1,7 +1,11 @@
import { nls } from '@theia/core/lib/common';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import {
Contribution,
Command,
@ -10,17 +14,11 @@ import {
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable()
export class EditContributions extends Contribution {
@inject(MonacoEditorService)
private readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
@ -208,9 +206,10 @@ ${value}
protected async current(): Promise<
ICodeEditor | StandaloneCodeEditor | undefined
> {
const codeEditorService = StandaloneServices.get(ICodeEditorService);
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor() ||
codeEditorService.getFocusedCodeEditor() ||
codeEditorService.getActiveCodeEditor() ||
undefined
);
}

View File

@ -1,11 +1,7 @@
import * as PQueue from 'p-queue';
import PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
import {
MenuPath,
CompositeMenuNode,
SubMenuOptions,
} from '@theia/core/lib/common/menu';
import { MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
import {
Disposable,
DisposableCollection,
@ -32,6 +28,8 @@ import {
CoreService,
SketchesService,
Sketch,
isBoardIdentifierChangeEvent,
BoardIdentifier,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
import { unregisterSubmenu } from '../menu/arduino-menus';
@ -112,7 +110,7 @@ export abstract class Examples extends SketchContribution {
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@ -121,12 +119,14 @@ export abstract class Examples extends SketchContribution {
protected override init(): void {
super.init();
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.handleBoardChanged(event.selectedBoard);
}
});
this.notificationCenter.onDidReinitialize(() =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
// No force refresh. The core client was already refreshed.
})
);
@ -138,24 +138,11 @@ export abstract class Examples extends SketchContribution {
}
protected abstract update(options?: {
board?: Board | undefined;
board?: BoardIdentifier | undefined;
forceRefresh?: boolean;
}): void;
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
const groupPath =
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
}
// Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(
@ -242,7 +229,7 @@ export abstract class Examples extends SketchContribution {
protected createHandler(uri: string): CommandHandler {
const forceUpdate = () =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
forceRefresh: true,
});
return {
@ -313,8 +300,8 @@ export class LibraryExamples extends Examples {
this.notificationCenter.onLibraryDidUninstall(() => this.update());
}
override async onReady(): Promise<void> {
this.update(); // no `await`
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.update());
}
protected override handleBoardChanged(board: Board | undefined): void {
@ -323,7 +310,7 @@ export class LibraryExamples extends Examples {
protected override async update(
options: { board?: Board; forceRefresh?: boolean } = {
board: this.boardsServiceClient.boardsConfig.selectedBoard,
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
}
): Promise<void> {
const { board, forceRefresh } = options;

View File

@ -1,8 +1,7 @@
import * as PQueue from 'p-queue';
import PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import {
Disposable,
@ -22,31 +21,28 @@ import { CurrentSketch } from '../sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
private readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
private readonly libraryService: LibraryService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
private readonly toDispose = new DisposableCollection();
override onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(() =>
this.boardsServiceProvider.onBoardsConfigDidChange(() =>
this.updateMenuActions()
);
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
@ -56,8 +52,8 @@ export class IncludeLibrary extends SketchContribution {
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
}
override async onReady(): Promise<void> {
this.updateMenuActions();
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.updateMenuActions());
}
override registerMenus(registry: MenuModelRegistry): void {
@ -93,12 +89,12 @@ export class IncludeLibrary extends SketchContribution {
});
}
protected async updateMenuActions(): Promise<void> {
private async updateMenuActions(): Promise<void> {
return this.queue.add(async () => {
this.toDispose.dispose();
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
const fqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));
@ -139,7 +135,7 @@ export class IncludeLibrary extends SketchContribution {
});
}
protected registerLibrary(
private registerLibrary(
libraryOrPlaceholder: LibraryPackage | string,
menuPath: MenuPath
): Disposable {
@ -172,7 +168,7 @@ export class IncludeLibrary extends SketchContribution {
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
private async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;

View File

@ -1,43 +1,116 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Mutex } from 'async-mutex';
import {
ArduinoDaemon,
assertSanitizedFqbn,
BoardIdentifier,
BoardsService,
CompileSummary,
ExecutableService,
isBoardIdentifierChangeEvent,
sanitizeFqbn,
} from '../../common/protocol';
import { CurrentSketch } from '../sketches-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { SketchContribution, URI } from './contribution';
import {
defaultAsyncWorkers,
maxAsyncWorkers,
minAsyncWorkers,
} from '../arduino-preferences';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginEvents } from '../hosted/hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution, URI } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
interface DaemonAddress {
/**
* The host where the Arduino CLI daemon is available.
*/
readonly hostname: string;
/**
* The port where the Arduino CLI daemon is listening.
*/
readonly port: number;
/**
* The [id](https://arduino.github.io/arduino-cli/latest/rpc/commands/#instance) of the initialized core Arduino client instance.
*/
readonly instance: number;
}
interface StartLanguageServerParams {
/**
* Absolute filesystem path to the Arduino Language Server executable.
*/
readonly lsPath: string;
/**
* The hostname and the port for the gRPC channel connecting to the Arduino CLI daemon.
* The `instance` number is for the initialized core Arduino client.
*/
readonly daemonAddress: DaemonAddress;
/**
* Absolute filesystem path to [`clangd`](https://clangd.llvm.org/).
*/
readonly clangdPath: string;
/**
* The board is relevant to start a specific "flavor" of the language.
*/
readonly board: { fqbn: string; name?: string };
/**
* `true` if the LS should generate the log files into the default location. The default location is the `cwd` of the process.
* It's very often the same as the workspace root of the IDE, aka the sketch folder.
* When it is a string, it is the absolute filesystem path to the folder to generate the log files.
* If `string`, but the path is inaccessible, the log files will be generated into the default location.
*/
readonly log?: boolean | string;
/**
* Optional `env` for the language server process.
*/
readonly env?: NodeJS.ProcessEnv;
/**
* Additional flags for the Arduino Language server process.
*/
readonly flags?: readonly string[];
/**
* Set to `true`, to enable `Diagnostics`.
*/
readonly realTimeDiagnostics?: boolean;
/**
* If `true`, the logging is not forwarded to the _Output_ view via the language client.
*/
readonly silentOutput?: boolean;
/**
* Number of async workers used by `clangd`. Background index also uses this many workers. If `0`, `clangd` uses all available cores. It's `0` by default.
*/
readonly jobs?: number;
}
/**
* The FQBN the language server runs with or `undefined` if it could not start.
*/
type StartLanguageServerResult = string | undefined;
@injectable()
export class InoLanguage extends SketchContribution {
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
private readonly executableService: ExecutableService;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex();
@ -45,7 +118,7 @@ export class InoLanguage extends SketchContribution {
override onReady(): void {
const start = (
{ selectedBoard }: BoardsConfig.Config,
selectedBoard: BoardIdentifier | undefined,
forceStart = false
) => {
if (selectedBoard) {
@ -56,12 +129,16 @@ export class InoLanguage extends SketchContribution {
}
};
const forceRestart = () => {
start(this.boardsServiceProvider.boardsConfig, true);
start(this.boardsServiceProvider.boardsConfig.selectedBoard, true);
};
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigChanged(start),
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
start(event.selectedBoard);
}
}),
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig)
start(this.boardsServiceProvider.boardsConfig.selectedBoard)
),
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
@ -72,6 +149,7 @@ export class InoLanguage extends SketchContribution {
switch (preferenceName) {
case 'arduino.language.log':
case 'arduino.language.realTimeDiagnostics':
case 'arduino.language.asyncWorkers':
forceRestart();
}
}
@ -82,28 +160,37 @@ export class InoLanguage extends SketchContribution {
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onChanged((dataChangePerFqbn) => {
this.boardDataStore.onDidChange((event) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const matchingFqbn = dataChangePerFqbn.find(
(fqbn) => sanitizedFqbn === fqbn
const sanitizedFQBN = sanitizeFqbn(this.languageServerFqbn);
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison.
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328
const matchingChange = event.changes.find(
(change) => sanitizedFQBN === sanitizeFqbn(change.fqbn)
);
const { boardsConfig } = this.boardsServiceProvider;
if (
matchingFqbn &&
boardsConfig.selectedBoard?.fqbn === matchingFqbn
matchingChange &&
boardsConfig.selectedBoard?.fqbn === matchingChange.fqbn
) {
start(boardsConfig);
start(boardsConfig.selectedBoard);
}
}
}),
this.compileSummaryProvider.onDidChangeCompileSummary(
(compileSummary) => {
if (compileSummary) {
this.fireBuildDidComplete(compileSummary);
}
}
),
]);
start(this.boardsServiceProvider.boardsConfig);
Promise.all([
this.boardsServiceProvider.ready,
this.preferences.ready,
]).then(() => {
start(this.boardsServiceProvider.boardsConfig.selectedBoard);
});
}
onStop(): void {
@ -116,10 +203,11 @@ export class InoLanguage extends SketchContribution {
forceStart = false
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
if (typeof port !== 'number') {
return;
}
const release = await this.languageServerStartMutex.acquire();
const toDisposeOnRelease = new DisposableCollection();
try {
await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
@ -147,7 +235,6 @@ export class InoLanguage extends SketchContribution {
}
return;
}
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(
@ -158,11 +245,16 @@ export class InoLanguage extends SketchContribution {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
const log = this.preferences.get('arduino.language.log');
const realTimeDiagnostics = this.preferences.get(
'arduino.language.realTimeDiagnostics'
);
const jobs = this.getAsyncWorkersPreferenceSafe();
this.logger.info(
`Starting language server: ${fqbnWithConfig}${
jobs ? ` (async worker count: ${jobs})` : ''
}`
);
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
@ -179,34 +271,89 @@ export class InoLanguage extends SketchContribution {
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
new Promise<undefined>((_, reject) => {
const timer = setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandService.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn: fqbnWithConfig,
name: name ? `"${name}"` : undefined,
},
realTimeDiagnostics,
silentOutput: true,
}
),
);
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
}),
this.start({
lsPath,
daemonAddress: {
hostname: 'localhost',
port,
instance: 1, // TODO: get it from the backend
},
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
board: {
fqbn: fqbnWithConfig,
name,
},
realTimeDiagnostics,
silentOutput: true,
jobs,
}),
]);
} catch (e) {
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
toDisposeOnRelease.dispose();
release();
}
}
// The Theia preference UI validation is bogus.
// To restrict the number of jobs to a valid value.
private getAsyncWorkersPreferenceSafe(): number {
const jobs = this.preferences.get(
'arduino.language.asyncWorkers',
defaultAsyncWorkers
);
if (jobs < minAsyncWorkers) {
return minAsyncWorkers;
}
if (jobs > maxAsyncWorkers) {
return maxAsyncWorkers;
}
return jobs;
}
private async start(
params: StartLanguageServerParams
): Promise<StartLanguageServerResult | undefined> {
return this.commandService.executeCommand<StartLanguageServerResult>(
'arduino.languageserver.start',
params
);
}
// Execute the a command contributed by the Arduino Tools VSIX to send the `ino/buildDidComplete` notification to the language server
private async fireBuildDidComplete(
compileSummary: CompileSummary
): Promise<void> {
const params = {
...compileSummary,
};
console.info(
`Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify(
params.buildOutputUri
)}`
);
try {
await this.commandService.executeCommand(
'arduino.languageserver.notifyBuildDidComplete',
params
);
} catch (err) {
console.error(
`Unexpected error when firing event on build did complete. ${JSON.stringify(
params.buildOutputUri
)}`,
err
);
}
}
}

View File

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

View File

@ -1,25 +1,18 @@
import { CommandRegistry } from '@theia/core';
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution, Command } from './contribution';
import { Contribution } from './contribution';
@injectable()
export class OpenBoardsConfig extends Contribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsConfigDialog)
private readonly boardsConfigDialog: BoardsConfigDialog;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
return (this.boardsServiceProvider.boardsConfig = boardsConfig);
}
},
execute: async (params?: EditBoardsConfigActionParams) =>
this.boardsConfigDialog.open(true, params),
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ApplicationError } from '@theia/core/lib/common/application-error';
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 { SketchesError } from '../../common/protocol';
import { StartupTasks } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
@ -25,6 +26,7 @@ import {
RenameCloudSketch,
RenameCloudSketchParams,
} from './rename-cloud-sketch';
import { assertConnectedToBackend } from './save-sketch';
@injectable()
export class SaveAsSketch extends CloudSketchContribution {
@ -35,7 +37,29 @@ export class SaveAsSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args),
execute: async (args) => {
try {
return await this.saveAs(args);
} catch (err) {
let message = String(err);
if (ApplicationError.is(err)) {
if (SketchesError.SketchAlreadyContainsThisFile.is(err)) {
message = nls.localize(
'arduino/sketch/sketchAlreadyContainsThisFileMessage',
'Failed to save sketch "{0}" as "{1}". {2}',
err.data.sourceSketchName,
err.data.targetSketchName,
err.message
);
} else {
message = err.message;
}
} else if (err instanceof Error) {
message = err.message;
}
this.messageService.error(message);
}
},
});
}
@ -58,13 +82,18 @@ export class SaveAsSketch extends CloudSketchContribution {
* Resolves `true` if the sketch was successfully saved as something.
*/
private async saveAs(
{
params = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const {
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
} = params;
assertConnectedToBackend({
connectionStatusService: this.connectionStatusService,
messageService: this.messageService,
});
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return false;
@ -95,7 +124,7 @@ export class SaveAsSketch extends CloudSketchContribution {
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
const options: WorkspaceInput & StartupTask.Owner = {
const options: WorkspaceInput & StartupTasks = {
preserveWindow: true,
tasks: [],
};
@ -165,16 +194,13 @@ export class SaveAsSketch extends CloudSketchContribution {
): Promise<string | undefined> {
let sketchFolderDestinationUri: string | undefined;
while (!sketchFolderDestinationUri) {
const { filePath } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
}
);
const { filePath } = await this.dialogService.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
if (!filePath) {
return undefined;
}
@ -225,13 +251,10 @@ ${dialogContent.details}
${dialogContent.question}`.trim();
defaultPath = filePath;
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message,
buttons: [Dialog.CANCEL, Dialog.YES],
}
);
const { response } = await this.dialogService.showMessageBox({
message,
buttons: [Dialog.CANCEL, Dialog.YES],
});
// cancel
if (response === 0) {
return undefined;

View File

@ -1,16 +1,18 @@
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import {
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
SketchContribution,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch';
@injectable()
export class SaveSketch extends SketchContribution {
@ -36,6 +38,10 @@ export class SaveSketch extends SketchContribution {
}
async saveSketch(): Promise<void> {
assertConnectedToBackend({
connectionStatusService: this.connectionStatusService,
messageService: this.messageService,
});
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
@ -63,3 +69,18 @@ export namespace SaveSketch {
};
}
}
// https://github.com/arduino/arduino-ide/issues/2081
export function assertConnectedToBackend(param: {
connectionStatusService: ApplicationConnectionStatusContribution;
messageService: MessageService;
}): void {
if (param.connectionStatusService.offlineStatus === 'backend') {
const message = nls.localize(
'theia/core/couldNotSave',
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
);
param.messageService.error(message);
throw new Error(message);
}
}

View File

@ -4,7 +4,10 @@ import {
} from '@theia/core/lib/browser/status-bar/status-bar';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { BoardsConfig } from '../boards/boards-config';
import type {
BoardList,
BoardListItem,
} from '../../common/protocol/board-list';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution } from './contribution';
@ -12,21 +15,23 @@ import { Contribution } from './contribution';
export class SelectedBoard extends Contribution {
@inject(StatusBar)
private readonly statusBar: StatusBar;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
override onStart(): void {
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
this.update(config)
this.boardsServiceProvider.onBoardListDidChange((boardList) =>
this.update(boardList)
);
}
override onReady(): void {
this.update(this.boardsServiceProvider.boardsConfig);
this.boardsServiceProvider.ready.then(() => this.update());
}
private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void {
private update(
boardList: BoardList = this.boardsServiceProvider.boardList
): void {
const { selectedBoard, selectedPort } = boardList.boardsConfig;
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
@ -38,17 +43,30 @@ export class SelectedBoard extends Contribution {
className: 'arduino-selected-board',
});
if (selectedBoard) {
const notConnectedLabel = nls.localize(
'arduino/common/notConnected',
'[not connected]'
);
let portLabel = notConnectedLabel;
if (selectedPort) {
portLabel = nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
);
const selectedItem: BoardListItem | undefined =
boardList.items[boardList.selectedIndex];
if (!selectedItem) {
portLabel += ` ${notConnectedLabel}`; // append ` [not connected]` when the port is selected but it's not detected by the CLI
}
}
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
)
: nls.localize('arduino/common/notConnected', '[not connected]'),
text: portLabel,
className: 'arduino-selected-port',
});
} else {
this.statusBar.removeElement('arduino-selected-port');
}
}
}

View File

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

View File

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

View File

@ -1,78 +0,0 @@
import { MessageService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { LocalStorageService } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SurveyNotificationService } from '../../common/protocol/survey-service';
const SURVEY_MESSAGE = nls.localize(
'arduino/survey/surveyMessage',
'Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better.'
);
const DO_NOT_SHOW_AGAIN = nls.localize(
'arduino/survey/dismissSurvey',
"Don't show again"
);
const GO_TO_SURVEY = nls.localize(
'arduino/survey/answerSurvey',
'Answer survey'
);
const SURVEY_BASE_URL = 'https://surveys.hotjar.com/';
const surveyId = '17887b40-e1f0-4bd6-b9f0-a37f229ccd8b';
@injectable()
export class SurveyNotification implements FrontendApplicationContribution {
@inject(MessageService)
private readonly messageService: MessageService;
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(WindowService)
private readonly windowService: WindowService;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SurveyNotificationService)
private readonly surveyNotificationService: SurveyNotificationService;
onStart(): void {
this.arduinoPreferences.ready.then(async () => {
if (
(await this.surveyNotificationService.isFirstInstance()) &&
this.arduinoPreferences.get('arduino.survey.notification')
) {
const surveyAnswered = await this.localStorageService.getData(
this.surveyKey(surveyId)
);
if (surveyAnswered !== undefined) {
return;
}
const answer = await this.messageService.info(
SURVEY_MESSAGE,
DO_NOT_SHOW_AGAIN,
GO_TO_SURVEY
);
switch (answer) {
case GO_TO_SURVEY:
this.windowService.openNewWindow(SURVEY_BASE_URL + surveyId, {
external: true,
});
this.localStorageService.setData(this.surveyKey(surveyId), true);
break;
case DO_NOT_SHOW_AGAIN:
this.localStorageService.setData(this.surveyKey(surveyId), false);
break;
}
}
});
}
private surveyKey(id: string): string {
return `answered_survey:${id}`;
}
}

View File

@ -0,0 +1,189 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { ArduinoState } from 'vscode-arduino-api';
import {
BoardsConfig,
BoardsService,
CompileSummary,
PortIdentifier,
resolveDetectedPort,
} from '../../common/protocol';
import {
toApiBoardDetails,
toApiCompileSummary,
toApiPort,
} from '../../common/protocol/arduino-context-mapper';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
/**
* (non-API) exported for tests
*/
export interface UpdateStateParams<T extends ArduinoState = ArduinoState> {
readonly key: keyof T;
readonly value: T[keyof T];
}
/**
* Contribution for updating the Arduino state, such as the FQBN, selected port, and sketch path changes via commands, so other VS Code extensions can access it.
* See [`vscode-arduino-api`](https://github.com/dankeboy36/vscode-arduino-api#api) for more details.
*/
@injectable()
export class UpdateArduinoState extends SketchContribution {
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection();
override onStart(): void {
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange(() =>
this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig)
),
this.sketchServiceClient.onCurrentSketchDidChange((sketch) =>
this.updateSketchPath(sketch)
),
this.configService.onDidChangeDataDirUri((dataDirUri) =>
this.updateDataDirPath(dataDirUri)
),
this.configService.onDidChangeSketchDirUri((userDirUri) =>
this.updateUserDirPath(userDirUri)
),
this.compileSummaryProvider.onDidChangeCompileSummary(
(compilerSummary) => {
if (compilerSummary) {
this.updateCompileSummary(compilerSummary);
}
}
),
this.boardsDataStore.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (
selectedFqbn &&
event.changes.find((change) => change.fqbn === selectedFqbn)
) {
this.updateBoardDetails(selectedFqbn);
}
}),
]);
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() => {
this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig);
});
this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch());
this.updateUserDirPath(this.configService.tryGetSketchDirUri());
this.updateDataDirPath(this.configService.tryGetDataDirUri());
const { compileSummary } = this.compileSummaryProvider;
if (compileSummary) {
this.updateCompileSummary(compileSummary);
}
}
onStop(): void {
this.toDispose.dispose();
}
private async updateSketchPath(
sketch: CurrentSketch | undefined
): Promise<void> {
const sketchPath = CurrentSketch.isValid(sketch)
? new URI(sketch.uri).path.fsPath()
: undefined;
return this.updateState({ key: 'sketchPath', value: sketchPath });
}
private async updateCompileSummary(
compileSummary: CompileSummary
): Promise<void> {
const apiCompileSummary = toApiCompileSummary(compileSummary);
return this.updateState({
key: 'compileSummary',
value: apiCompileSummary,
});
}
private async updateBoardsConfig(boardsConfig: BoardsConfig): Promise<void> {
const fqbn = boardsConfig.selectedBoard?.fqbn;
const port = boardsConfig.selectedPort;
await this.updateFqbn(fqbn);
await this.updateBoardDetails(fqbn);
await this.updatePort(port);
}
private async updateFqbn(fqbn: string | undefined): Promise<void> {
await this.updateState({ key: 'fqbn', value: fqbn });
}
private async updateBoardDetails(fqbn: string | undefined): Promise<void> {
const unset = () =>
this.updateState({ key: 'boardDetails', value: undefined });
if (!fqbn) {
return unset();
}
const [details, persistedData] = await Promise.all([
this.boardsService.getBoardDetails({ fqbn }),
this.boardsDataStore.getData(fqbn),
]);
if (!details) {
return unset();
}
const apiBoardDetails = toApiBoardDetails({
...details,
configOptions:
BoardsDataStore.Data.EMPTY === persistedData
? details.configOptions
: persistedData.configOptions.slice(),
});
return this.updateState({
key: 'boardDetails',
value: apiBoardDetails,
});
}
private async updatePort(port: PortIdentifier | undefined): Promise<void> {
const resolvedPort =
port &&
resolveDetectedPort(port, this.boardsServiceProvider.detectedPorts);
const apiPort = resolvedPort && toApiPort(resolvedPort);
return this.updateState({ key: 'port', value: apiPort });
}
private async updateUserDirPath(userDirUri: URI | undefined): Promise<void> {
const userDirPath = userDirUri?.path.fsPath();
return this.updateState({
key: 'userDirPath',
value: userDirPath,
});
}
private async updateDataDirPath(dataDirUri: URI | undefined): Promise<void> {
const dataDirPath = dataDirUri?.path.fsPath();
return this.updateState({
key: 'dataDirPath',
value: dataDirPath,
});
}
private async updateState<T extends ArduinoState>(
params: UpdateStateParams<T>
): Promise<void> {
await this.hostedPluginSupport.didStart;
return this.commandService.executeCommand('arduinoAPI.updateState', params);
}
}

View File

@ -16,7 +16,10 @@ import {
arduinoCert,
certificateList,
} from '../dialogs/certificate-uploader/utils';
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
import {
ArduinoFirmwareUploader,
UploadCertificateParams,
} from '../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
@injectable()
@ -74,12 +77,8 @@ export class UploadCertificate extends Contribution {
});
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
execute: async ({ fqbn, address, urls }) => {
return this.arduinoFirmwareUploader.uploadCertificates(
`-b ${fqbn} -a ${address} ${urls
.map((url: string) => `-u ${url}`)
.join(' ')}`
);
execute: async (params: UploadCertificateParams) => {
return this.arduinoFirmwareUploader.uploadCertificates(params);
},
});

View File

@ -45,10 +45,7 @@ export namespace UploadFirmware {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-firmware-open',
label: nls.localize(
'arduino/firmware/updater',
'WiFi101 / WiFiNINA Firmware Updater'
),
label: nls.localize('arduino/firmware/updater', 'Firmware Updater'),
category: 'Arduino',
};
}

View File

@ -1,30 +1,31 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService, Port, sanitizeFqbn } from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
CoreServiceContribution,
KeybindingRegistry,
MenuModelRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';
import type { VerifySketchParams } from './verify-sketch';
@injectable()
export class UploadSketch extends CoreServiceContribution {
@inject(UserFields)
private readonly userFields: UserFields;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
@inject(UserFields)
private readonly userFields: UserFields;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
@ -103,11 +104,11 @@ export class UploadSketch extends CoreServiceContribution {
}
try {
const autoVerify = this.preferences['arduino.upload.autoVerify'];
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.menuManager.update();
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire();
this.clearVisibleNotification();
@ -116,7 +117,7 @@ export class UploadSketch extends CoreServiceContribution {
'arduino-verify-sketch',
<VerifySketchParams>{
exportBinaries: false,
silent: true,
mode: autoVerify ? 'auto' : 'dry-run',
}
);
if (!verifyOptions) {
@ -127,6 +128,7 @@ export class UploadSketch extends CoreServiceContribution {
usingProgrammer,
verifyOptions
);
if (!uploadOptions) {
return;
}
@ -135,13 +137,42 @@ export class UploadSketch extends CoreServiceContribution {
return;
}
await this.doWithProgress({
const uploadResponse = await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: (progressId, coreService) =>
coreService.upload({ ...uploadOptions, progressId }),
task: async (progressId, coreService, token) => {
try {
return await coreService.upload(
{ ...uploadOptions, progressId },
token
);
} catch (err) {
if (err.code === 4005) {
const uploadWithProgrammerOptions = await this.uploadOptions(
true,
verifyOptions
);
if (uploadWithProgrammerOptions) {
return coreService.upload(
{ ...uploadWithProgrammerOptions, progressId },
token
);
}
} else {
throw err;
}
}
},
keepOutput: true,
cancelable: true,
});
if (!uploadResponse) {
return;
}
// the port update is NOOP if nothing has changed
this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload);
this.messageService.info(
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
{ timeout: 3000 }
@ -150,9 +181,10 @@ export class UploadSketch extends CoreServiceContribution {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
// TODO: here comes the port change if happened during the upload
// https://github.com/arduino/arduino-cli/issues/2245
this.uploadInProgress = false;
this.menuManager.update();
this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire();
}
}
@ -170,11 +202,15 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
this.boardsDataStore.getData(
verifyOptions.fqbn
? new FQBN(verifyOptions.fqbn).toString(true)
: undefined
),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
const port = boardsConfig.selectedPort;
return {
sketch,
fqbn,
@ -185,28 +221,6 @@ export class UploadSketch extends CoreServiceContribution {
userFields,
};
}
/**
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
*
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
*/
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
if (port) {
const key = Port.keyOf(port);
for (const candidate of this.boardsServiceProvider.availablePorts) {
if (key === Port.keyOf(candidate) && candidate.properties) {
return {
...port,
properties: deepClone(candidate.properties),
};
}
}
}
return port;
}
}
export namespace UploadSketch {

View File

@ -1,10 +1,10 @@
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution';
import { Contribution, MenuModelRegistry } from './contribution';
import { UploadSketch } from './upload-sketch';
@injectable()
@ -21,12 +21,11 @@ export class UserFields extends Contribution {
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.menuManager.update();
});
this.boardsServiceProvider.onBoardsConfigDidChange(() => this.refresh());
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.refresh());
}
override registerMenus(registry: MenuModelRegistry): void {
@ -37,16 +36,20 @@ export class UserFields extends Contribution {
});
}
private async refresh(): Promise<void> {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.menuManager.update();
}
private selectedFqbnAddress(): string | undefined {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return undefined;
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address ||
'';
const address = boardsConfig.selectedPort?.address || '';
return fqbn + '|' + address;
}

View File

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

View File

@ -1,46 +1,71 @@
import { Emitter, Event } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import type { CompileSummary, CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
CoreServiceContribution,
KeybindingRegistry,
MenuModelRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler';
export const CompileSummaryProvider = Symbol('CompileSummaryProvider');
export interface CompileSummaryProvider {
readonly compileSummary: CompileSummary | undefined;
readonly onDidChangeCompileSummary: Event<CompileSummary | undefined>;
}
export type VerifySketchMode =
/**
* When the user explicitly triggers the verify command from the primary UI: menu, toolbar, or keybinding. The UI shows the output, updates the toolbar items state, etc.
*/
| 'explicit'
/**
* When the verify phase automatically runs as part of the upload but there is no UI indication of the command: the toolbar items do not update.
*/
| 'auto'
/**
* The verify does not run. There is no UI indication of the command. For example, when the user decides to disable the auto verify (`'arduino.upload.autoVerify'`) to skips the code recompilation phase.
*/
| 'dry-run';
export interface VerifySketchParams {
/**
* Same as `CoreService.Options.Compile#exportBinaries`
*/
readonly exportBinaries?: boolean;
/**
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
* The mode specifying how verify should run. It's `'explicit'` by default.
*/
readonly silent?: boolean;
readonly mode?: VerifySketchMode;
}
/**
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
* - `"idle"` when neither verify, nor upload is running
*/
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
type VerifyProgress = 'idle' | VerifySketchMode;
@injectable()
export class VerifySketch extends CoreServiceContribution {
export class VerifySketch
extends CoreServiceContribution
implements CompileSummaryProvider
{
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private readonly onDidChangeCompileSummaryEmitter = new Emitter<
CompileSummary | undefined
>();
private verifyProgress: VerifyProgress = 'idle';
private _compileSummary: CompileSummary | undefined;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
@ -54,10 +79,10 @@ export class VerifySketch extends CoreServiceContribution {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => this.verifyProgress !== 'explicit-verify',
isEnabled: () => this.verifyProgress !== 'explicit',
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit-verify',
isToggled: () => this.verifyProgress === 'explicit',
execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
});
@ -105,6 +130,21 @@ export class VerifySketch extends CoreServiceContribution {
super.handleError(error);
}
get compileSummary(): CompileSummary | undefined {
return this._compileSummary;
}
private updateCompileSummary(
compileSummary: CompileSummary | undefined
): void {
this._compileSummary = compileSummary;
this.onDidChangeCompileSummaryEmitter.fire(this._compileSummary);
}
get onDidChangeCompileSummary(): Event<CompileSummary | undefined> {
return this.onDidChangeCompileSummaryEmitter.event;
}
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
@ -113,34 +153,44 @@ export class VerifySketch extends CoreServiceContribution {
}
try {
this.verifyProgress = params?.silent
? 'automatic-verify'
: 'explicit-verify';
this.verifyProgress = params?.mode ?? 'explicit';
this.onDidChangeEmitter.fire();
this.menuManager.update();
this.clearVisibleNotification();
this.coreErrorHandler.reset();
const dryRun = this.verifyProgress === 'dry-run';
const options = await this.options(params?.exportBinaries);
if (!options) {
return undefined;
}
await this.doWithProgress({
if (dryRun) {
return options;
}
const compileSummary = await this.doWithProgress({
progressText: nls.localize(
'arduino/sketch/compile',
'Compiling sketch...'
),
task: (progressId, coreService) =>
coreService.compile({
...options,
progressId,
}),
task: (progressId, coreService, token) =>
coreService.compile(
{
...options,
progressId,
},
token
),
cancelable: true,
});
this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 }
);
this.updateCompileSummary(compileSummary);
// Returns with the used options for the compilation
// so that follow-up tasks (such as upload) can reuse the compiled code.
// Note that the `fqbn` is already decorated with the board settings, if any.

View File

@ -86,26 +86,25 @@ export class CreateApi {
url.searchParams.set('user_id', 'me');
url.searchParams.set('limit', limit.toString());
const headers = await this.headers();
const result: { sketches: Create.Sketch[] } = { sketches: [] };
let partialSketches: Create.Sketch[] = [];
const allSketches: Create.Sketch[] = [];
let currentOffset = 0;
do {
while (true) {
url.searchParams.set('offset', currentOffset.toString());
partialSketches = (
await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
})
).sketches;
if (partialSketches.length !== 0) {
result.sketches = result.sketches.concat(partialSketches);
const { sketches } = await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
allSketches.push(...sketches);
if (sketches.length < limit) {
break;
}
currentOffset = currentOffset + limit;
} while (partialSketches.length !== 0);
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;
currentOffset += limit;
// The create API doc show that there is `next` and `prev` pages, but it does not work
// https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_search
// IF sketchCount mod limit === 0, an extra fetch must happen to detect the end of the pagination.
}
allSketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return allSketches;
}
async createSketch(
@ -180,7 +179,8 @@ export class CreateApi {
);
})
.catch((reason) => {
if (reason?.status === 404) return [] as Create.Resource[];
if (reason?.status === 404)
return [] as Create.Resource[]; // TODO: must not swallow 404
else throw reason;
});
}
@ -487,18 +487,12 @@ 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> {
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 {
@ -509,28 +503,25 @@ 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;
}
private async headers(): Promise<Record<string, string>> {
const token = await this.token();
return {
const headers: Record<string, string> = {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
};
const sharedSpaceID =
this.arduinoPreferences['arduino.cloud.sharedSpaceID'];
if (sharedSpaceID) {
headers['x-organization'] = sharedSpaceID;
}
return headers;
}
private domain(apiVersion = 'v2'): string {

View File

@ -1,13 +1,17 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Sketch } from '../../common/protocol';
import { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import {
ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER,
} from '../utils/constants';
import { CreateUri } from './create-uri';
export type CloudSketchState = 'push' | 'pull';
@ -128,8 +132,8 @@ export class CreateFeatures implements FrontendApplicationContribution {
return undefined;
}
return dataDirUri
.resolve('RemoteSketchbook')
.resolve('ArduinoCloud')
.resolve(REMOTE_SKETCHBOOK_FOLDER)
.resolve(ARDUINO_CLOUD_FOLDER)
.isEqualOrParent(new URI(sketch.uri));
}

View File

@ -5,7 +5,7 @@ import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import {
Stat,
FileType,

View File

@ -82,6 +82,13 @@ export function isNotFound(err: unknown): err is NotFoundError {
return isErrorWithStatusOf(err, 404);
}
export type UnprocessableContentError = CreateError & { status: 422 };
export function isUnprocessableContent(
err: unknown
): err is UnprocessableContentError {
return isErrorWithStatusOf(err, 422);
}
function isErrorWithStatusOf(
err: unknown,
status: number

View File

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

View File

@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import * as React from '@theia/core/shared/react';
import React from '@theia/core/shared/react';
export const CertificateAddComponent = ({
addCertificate,

View File

@ -1,4 +1,4 @@
import * as React from '@theia/core/shared/react';
import React from '@theia/core/shared/react';
export const CertificateListComponent = ({
certificates,

View File

@ -1,20 +1,27 @@
import * as React from '@theia/core/shared/react';
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
import { SelectBoardComponent } from './select-board-components';
import type { BoardList } from '../../../common/protocol/board-list';
import {
boardIdentifierEquals,
portIdentifierEquals,
} from '../../../common/protocol/boards-service';
import { CertificateAddComponent } from './certificate-add-new';
import { nls } from '@theia/core/lib/common';
import { CertificateListComponent } from './certificate-list';
import {
BoardOptionValue,
SelectBoardComponent,
} from './select-board-components';
export const CertificateUploaderComponent = ({
availableBoards,
boardList,
certificates,
addCertificate,
updatableFqbns,
uploadCertificates,
openContextMenu,
}: {
availableBoards: AvailableBoard[];
boardList: BoardList;
certificates: string[];
addCertificate: (cert: string) => void;
updatableFqbns: string[];
@ -33,11 +40,15 @@ export const CertificateUploaderComponent = ({
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const [selectedItem, setSelectedItem] =
React.useState<BoardOptionValue | null>(null);
const installCertificates = async () => {
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
if (!selectedItem) {
return;
}
const board = selectedItem.board;
if (!board.fqbn) {
return;
}
@ -45,8 +56,8 @@ export const CertificateUploaderComponent = ({
try {
await uploadCertificates(
selectedBoard.fqbn,
selectedBoard.port.address,
board.fqbn,
selectedItem.port.address,
selectedCerts
);
setInstallFeedback('ok');
@ -55,17 +66,26 @@ export const CertificateUploaderComponent = ({
}
};
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
const onItemSelect = React.useCallback(
(item: BoardOptionValue | null) => {
if (!item) {
setSelectedItem(null);
return;
}
const board = item.board;
const port = item.port;
const selectedBoard = selectedItem?.board;
const selectedPort = selectedItem?.port;
if (newFqbn !== prevFqbn) {
if (
!boardIdentifierEquals(board, selectedBoard) ||
!portIdentifierEquals(port, selectedPort)
) {
setInstallFeedback(null);
setSelectedBoard(board);
setSelectedItem(item);
}
},
[selectedBoard]
[selectedItem]
);
return (
@ -125,10 +145,10 @@ export const CertificateUploaderComponent = ({
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
boardList={boardList}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
onItemSelect={onItemSelect}
selectedItem={selectedItem}
busy={installFeedback === 'installing'}
/>
</div>
@ -167,7 +187,7 @@ export const CertificateUploaderComponent = ({
type="button"
className="theia-button primary install-cert-btn"
onClick={installCertificates}
disabled={selectedCerts.length === 0 || !selectedBoard}
disabled={selectedCerts.length === 0 || !selectedItem}
>
{nls.localize('arduino/certificate/upload', 'Upload')}
</button>

View File

@ -1,62 +1,51 @@
import * as React from '@theia/core/shared/react';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { nls } from '@theia/core/lib/common/nls';
import type { Message } from '@theia/core/shared/@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import React from '@theia/core/shared/react';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { createBoardList } from '../../../common/protocol/board-list';
import { ArduinoPreferences } from '../../arduino-preferences';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { certificateList, sanifyCertString } from './utils';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
private readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
private certificates: string[] = [];
private updatableFqbns: string[] = [];
private boardList = createBoardList({});
public busyCallback = (busy: boolean) => {
busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoPreferences.ready.then(() => {
@ -81,8 +70,8 @@ export class UploadCertificateDialogWidget extends ReactWidget {
})
);
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.boardsServiceProvider.onBoardListDidChange((boardList) => {
this.boardList = boardList;
this.update();
});
}
@ -126,7 +115,7 @@ export class UploadCertificateDialogWidget extends ReactWidget {
protected render(): React.ReactNode {
return (
<CertificateUploaderComponent
availableBoards={this.availableBoards}
boardList={this.boardList}
certificates={this.certificates}
updatableFqbns={this.updatableFqbns}
addCertificate={this.addCertificate.bind(this)}
@ -143,7 +132,7 @@ export class UploadCertificateDialogProps extends DialogProps {}
@injectable()
export class UploadCertificateDialog extends AbstractDialog<void> {
@inject(UploadCertificateDialogWidget)
protected readonly widget: UploadCertificateDialogWidget;
private readonly widget: UploadCertificateDialogWidget;
private busy = false;

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