Compare commits

..

112 Commits

Author SHA1 Message Date
Akos Kitta
1dc7a89dd9 ATL-935: Better support for opening large projects
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
1280a344a7 ATL-806: Fixed always_export_binaries CLI config
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
f1c80041fe GH-432: Made compile/verify work on dirty editors
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
ec1abcc989 Fixed the build status badge in the main README.
Closes arduino/arduino-ide#7

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
1c03d12165 GH-423: Do not copy copyright from about dialog
Closes arduino/arduino-pro-ide#423

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
9180f4e378 Removed the 'Beta' status from title and about.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
516c79276c GH-430: Fixed 'Close' confirmation is ignored.
Updated to lates Theia: `1.11.0-next.c9db9754`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
e639d7da06 GH-422: Changed the default verbose mode.
From now on, compile/upload is not verbose.

Closes arduino/arduino-pro-ide#422.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
per1234
754097877b Make the example values in the bug report template more relevant
The bug report template provides some example values for OS and version. The previous examples were completely
irrelevant to this project due to using an unsupported OS and a different versioning scheme. This might cause confusion,
or at least seem odd, to the contributor.
2021-02-11 23:21:57 -08:00
per1234
b847cff615 Add issue templates
At the first step of creating an issue, a menu of issue types will be presented:

- Bug report
- Feature request
- Report a security vulnerability

If one of the first two are selected, the issue body field will be pre-filled with the template Markdown.

"Report a security vulnerability" will take them to Arduino's global security disclosure policy, which provides further guidance.

If none of the categories in the issue type chooser are applicable, the "Open a blank issue." link at the bottom of the
page can be selected, which will provide the non-templated issue creation experience.

These templates are copies of https://github.com/arduino/arduino-pro-ide/tree/master/.github/ISSUE_TEMPLATE
2021-02-11 23:21:57 -08:00
Akos Kitta
b3deb2fd34 Aligned workflow/docs with the default branch name
This is required after switching the default from `master` to `main`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 15:12:23 +01:00
Sebastian Romero
b2641f56be Remove underscore from product name 2021-02-11 11:03:36 +01:00
Akos Kitta
ba8885c8c8 ATL-938: Added menu group categories.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
3e92567d52 GH-421: Cleaned up the _Output_ channel UI.
- Merged the Arduino channels into one,
 - Removed the channel selector dropdown from the UI.

Closes arduino/arduino-pro-ide#421.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
19613de1b4 ATL-936: Fixed the theme dropdown in the settings.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
01ef138d9a ATL-551: Removed the _Advanced Mode_ toggle.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
39b8a602c7 [UX]: Fixed button styles with the HC theme.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
57c50fefe3 ATL-885: Refined the 'Close' behavior.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
23877f162c ATL-879: Avoid reopening the same sketch.
Instead of reopening it, focus the existing window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
96f0722d56 Removed the arduino-debugger-extension extension
We use the `cortex-debug` VSX.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Sebastian Romero
48c6c53b9b Add rebranded icons 2021-02-11 09:59:46 +01:00
Akos Kitta
b8647f16ad Renamed the application. Updated links and version
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
cfe9e8ec95 Bumped version to 0.1.4. Use CLI 0.15.2.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
291179489f Reenabled the nightly build.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 18:33:44 +01:00
Akos Kitta
71cfa06fc2 ATL-878: Fixed boards dropdown with the HC theme.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
68b1f8d4f2 Implemented the Network tab.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
e957ac4331 ATL-74: Added Export compiled Binary.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
b65867d2f4 ATL-58: Archive sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
a8e60698a8 ATL-836: Implemented 'Add File...'.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
52b0fd35a3 ATL-93: Added Support for .pde sketch file format.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
b1ab6df8b7 Reimplemented sketchbook watcher.
Moved it to the frontend.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
911875665d Do not bail when wiping the temp sketch has failed
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
113fe38850 Fixed the Views menu registration.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
770e0b592a Log sketchbook watch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
79bf0a123f Fixed the C++ extension download link.
It was a 404 due to some changes in Open VSX.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
8eaf03a299 Fixed the app packager on Windows.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
f36d261dcd [debug]: No await for the watcher in sketchbook.
This seems to block the workspace init on Windows in bundled electron.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
a99093624f Updated to 0.15.0-rc1 CLI and 12.x snapshot clangd.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
1f544b2656 ATL-546: Added UI for settings.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
1742c53015 ATL-812: Enhanced the Help menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
e33af0d78a Use init instead of dump for config fallback.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
24dfffa976 ATL-835: Support for JSON file type.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
6626701bc9 ATL-815: Implemented Open Recent.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
66b711f43c Made the CLI schema validation bit more relaxed.
Both `metrics` and `telemetry` are generated by `config dump`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
c6b125011e ATL-814: Show boards and ports under Tools menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
f6b5dd24e2 Patched the Theia menu factory.
eclipse-theia/theia#8977

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
14919bba1b Fixed whitespace issue in About dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
0b89cc4a3b Updated to the HEAD CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
284181b874 ATL-811: Removed Run menu item from the app menu
It came from the `@theia/debug` extension.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
db2967084f Added the Sketchbook menu with FS event tracking
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-18 13:22:38 +01:00
Akos Kitta
1b6d9eccdc Disabled the CRON (nightly build) job.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-17 17:32:51 +01:00
Akos Kitta
258b1e903e GH-393: Do not use clangd from the $PATH.
Closes: arduino/arduino-pro-ide#393

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-15 12:43:11 +01:00
Akos Kitta
00a3ee34c8 Added a script to update the versions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-14 15:33:06 +01:00
Akos Kitta
f1bffaab2d Fixed Save As when overwriting existing sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-11 12:56:27 +01:00
Akos Kitta
3191a09562 [ci]: Fixed the GH release action.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 20:15:40 +01:00
Akos Kitta
40905a058c Bumped version to 0.1.3.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
e7b1a27401 ATL-730: Refactored the debug extension.
Wired in the `cortex.debug` VSXE.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
4d5a046aa8 Switched to the '0.14.0' CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
c024a8d3d1 ATL-750: Handle board name change after install.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
7696e2c4c9 ATL-723: Show the build time in the about dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
1acf13c397 ATL-732: Support for static splash screen.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
cff2c95684 ATL-667: Warn the user when could not save sketch.
- Log the PID of the backend process.
 - Aligned the dev startup mode with the production: `--no-cluster`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
1a531db0b7 Disabled the badge decoration in the Explorer.
Ref: eclipse-theia/theia#8709
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
2e00e2db35 Added a workaround for Theia's auto-save issue.
Ref: eclipse-theia/theia#8722
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
ca1b288706 ATL-667: Show dirty indicator on unclosable widget
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-10 16:41:01 +01:00
Akos Kitta
41eeb337f9 ATL-675: Use the upstream GH Action for the upload
Ref: svenstaro/upload-release-action#25
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-17 08:26:52 +01:00
per1234
39b2e49edb Add EULA to Windows interactive installer
Reference: https://www.electron.build/configuration/nsis#NsisOptions-license
2020-11-13 01:05:55 -08:00
Akos Kitta
138afbf7fd ATL-469: Fixed various serial-monitor issues.
- Fixed a monitor reconnecting issue after upload.
 - Serial monitor connection was not disposed when the widget was closed
from the toolbar with the magnifier (🔍) icon. It worked only iff the
user closed the view with the `X`.
 - This commit also fixes a warning that was related to the incorrect focus
handling of the widget.
 - Switched to `board list -w` instead of polling.
 - Added a singleton for the board discovery to spare the CPU.
 - Fixed DI scopes on the backend. Each frontend gets its own service.
 - Switched to the `20201112` nightly CLI.
 - Fixed the Monitor view's image when the view is on the side-bar.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-12 18:53:58 +01:00
Akos Kitta
01e42dafde ATL-666: Added graphics for the Windows installer.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-12 11:53:00 +01:00
Akos Kitta
2831acc5b5 ATL-530: No checks before upload/verify/burn
Made the port/fqbn/programmer optional for upload, verify,
and burn bootloader. From now on, the IDE does not warn the user before
performing the desired CLI command.

Closes arduino/arduino-pro-ide#364

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
Akos Kitta
acbb7d32b2 ATL-428: Fixed the semver ordering for installable
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
Akos Kitta
781747fe80 Fixed the application name on macOS.
Patch for eclipse-theia/theia#8701.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
Akos Kitta
874c3efa2c ATL-663: Indicate alpha status. Updated the About dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
Akos Kitta
7b364ebe60 Use the CLI API from the 20201104 nightly.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
Akos Kitta
a96449f557 ATL-658: IDE can use any pinned version of CLI.
- Pinned the CLI to the `20201104` nightly.
 - Updated the TS/JS API generator to fall back to forks if configured.
 - Updated the CLI JSON schema.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-11-06 10:20:52 +01:00
per1234
c78e474790 Fix certificate check CI workflow's crontab
An error in the crontab configuration resulted in the `schedule` event triggered workflow running every 6-9 minutes (the minimum interval GitHub Actions provides) for the duration of every tenth hour.

The updated crontab causes the workflow to run once every 10 hours, as intended.
2020-10-26 02:39:14 -07:00
Akos Kitta
30136b0ef2 Capture and swallow unhandled SIGPIPE signal.
To be able to work around the backend process crash and offline status.

Ref: eclipse-theia/theia#8660
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-10-23 09:05:11 +02:00
per1234
53b06aef67 Add workflow to check for problems with certificates
If the macOS or Windows signing certificates fail verification, a notification will be posted on the #team_tooling Slack channel.

If the certificates expire in less than 30 days, a notification will be posted on the #team_tooling Slack channel.
2020-10-22 07:59:49 -07:00
per1234
6535c70686 Add signed MSI package to the "Arduino Pro IDE" workflow 2020-10-20 14:19:04 -07:00
per1234
6ff58ebe7c Use the windows-latest runner in the Arduino Pro IDE workflow
It was previously required to use the `windows-2016` runner to build Arduino Pro IDE. That is no longer necessary and
Windows signing fails when using that runner.
2020-10-20 14:19:04 -07:00
per1234
7068b9b1d3 Add signed Windows installer package to the "Arduino Pro IDE" workflow
The previous "zip" Windows package is retained, but an installer is also produced.
2020-10-20 14:19:04 -07:00
Akos Kitta
e755a1cd7e Aligned the electron app to the latest Theia APIs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-10-12 16:28:07 +02:00
Akos Kitta
def93ea32f GH-354: Moved the Outline to the left-hand side.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-10-12 16:28:07 +02:00
Akos Kitta
5f5193932f ATL-374: Refactored the Output services.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-10-12 16:28:07 +02:00
Akos Kitta
f26dae185b ATL-222: Moved the language feature to a VS Code extension.
Updated to next Theia: 1.6.0-next.b43a1623.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-10-12 16:28:07 +02:00
Akos Kitta
fbebfc7cca Use 0.13.0 CLI. Updated version to 0.1.2.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-14 14:14:37 +02:00
Akos Kitta
c3eb3e4622 arduino/arduino-pro-ide#337: Use bigger min window
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-10 10:45:38 +02:00
Akos Kitta
30421f0de4 Enabled file logging for the backend process.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-10 10:45:38 +02:00
Akos Kitta
daa25794ef Better error handling when killing the BE process.
Catch the ESRCH error when terminating non-existing backend process.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-10 10:45:38 +02:00
Akos Kitta
ec8df37c2d Fixed the output channel registry for extensions.
See: eclipse-theia/theia#8122

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-10 10:45:38 +02:00
Akos Kitta
cb24571eeb Customized channel to cancel the queue on dispose.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-10 10:45:38 +02:00
Akos Kitta
2f8e28b296 Patched the menu ordering. (Workaround for eclipse-theia/theia#8377)
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
524fbbdf40 arduino/arduino-pro-ide#336: Fixed 'Save As...'
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
7a37aa2e2f ATL-78: Implemented include library.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
56ff86629c ATL-73: Added library examples to the app.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
1c9fcd0cdf ATL-302: Added built-in examples to the app.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
b5d7c3b45d ATL-61: Implemented burn bootloader.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-07 13:42:11 +02:00
Akos Kitta
525e688d70 Use git log as of the body for the GH release.
There is no need to prepend any other info to the release body.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-04 13:12:41 +02:00
Akos Kitta
d7f4d0c18e Fixed the tag name of the GH releases.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-04 13:12:41 +02:00
Akos Kitta
6ae7404092 ATL-439: Create the GH release in the public repo.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-02 21:05:46 +02:00
Akos Kitta
6b1b9c0524 Use docker://plugins/s3 action for the S3 upload
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-02 21:05:46 +02:00
Akos Kitta
717db95c90 ATL-424: Generate a changelog for the nightlies.
Configure generated changelog output from `changelog` job so it can be used in the `release` job of the workflow

It is necessary to define job outputs to make them accessible via the `needs` context in other jobs.

Reference: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idneeds

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-02 21:05:46 +02:00
Akos Kitta
7536c3a485 ATL-423: Can execute the nightly manually.
We consider a build as nightly, if was started by the
CRON job, or was manually triggered from the master
branch.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-09-02 21:05:46 +02:00
Akos Kitta
ec7df884ee Merge pull request #143 from bcmi-labs/0.1.1
0.1.1
2020-08-26 16:22:34 +02:00
Akos Kitta
24b6d84d27 Updated the CLI to 0.12.1.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-26 15:23:24 +02:00
Akos Kitta
4435696949 Updated the versions from 0.1.0 to 0.1.1.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-26 15:13:04 +02:00
Akos Kitta
6aa3ff8044 Merge pull request #141 from bcmi-labs/arduino/arduino-pro-ide#311
GH-311: Fixed FS path to URI conversion issue.
2020-08-25 18:10:10 +02:00
Akos Kitta
4b44113f2c GH-311: Fixed FS path to URI conversion issue.
Instead of passing the FS path (`string`) to the `LoadSketch` method,
we have to convert it into a proper URI string.

Closes arduino/arduino-pro-ide#311

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-25 16:03:49 +02:00
Akos Kitta
c6ad0f582a Merge pull request #139 from bcmi-labs/build-on-ubuntu
Added steps to build the app from source
2020-08-24 20:37:27 +02:00
Akos Kitta
4b8b468e53 Added steps to build the app from source
Verified on 18.04.4

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-24 18:52:25 +02:00
Akos Kitta
ba319b23d9 Merge pull request #133 from bcmi-labs/per1234/name-snapshot-archive-with-commit
Use short commit hash in "snapshot" build artifact archive filename
2020-08-24 16:43:27 +02:00
per1234
70278fed6f Use short commit hash in "snapshot" build artifact archive filename
When you have multiple snapshot builds on your computer for testing purposes, it's difficult to keep track of which is which. The commit hash is shown by the running application, but it's more convenient to have the installation also identified by its folder name.

Adding the short commit hash to the build archive filename (and thus the extracted folder name) provides a clear identifier for each build.

Before this change, the archive is named like:
arduino-pro-ide_snapshot_Linux_64bit.zip

after:
arduino-pro-ide_0.0.7-snapshot.1b8c510_Linux_64bit.zip
2020-08-24 05:12:10 -07:00
224 changed files with 16224 additions and 9003 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Version: [e.g. 2.0.0]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,14 +1,15 @@
name: Arduino Pro IDE
name: Arduino IDE
on:
push:
branches:
- master
- main
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
pull_request:
branches:
- master
- main
schedule:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
@@ -18,23 +19,20 @@ jobs:
strategy:
matrix:
config:
- os: windows-2016
- os: windows-latest
- os: ubuntu-latest
- os: macos-latest
# - os: rsora-rpi-arm # self-hosted armhf
runs-on: ${{ matrix.config.os }}
timeout-minutes: 90
env:
CERTIFICATE_PATH: /tmp/macos_signing_certificate.p12
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js 10.x
- name: Install Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: '10.x'
node-version: '12.14.1'
registry-url: 'https://registry.npmjs.org'
- name: Install Python 2.7
@@ -42,31 +40,33 @@ jobs:
with:
python-version: '2.7'
- name: Generate signing certificate file [macOS]
if: runner.OS == 'macOS'
run: |
# 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
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"
- name: Package
shell: bash
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
# electron-builder will try to sign during the Windows job if these environment variables are defined
# See: https://www.electron.build/code-signing
if [ "${{ runner.OS }}" = "macOS" ]; then
# See: https://www.electron.build/code-signing
export CSC_LINK="${{ env.CERTIFICATE_PATH }}"
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
# 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
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
elif [ "${{ runner.OS }}" = "Windows" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
fi
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
@@ -76,9 +76,51 @@ jobs:
name: build-artifacts
path: electron/build/dist/build-artifacts/
publish:
changelog:
needs: build
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
outputs:
BODY: ${{ steps.changelog.outputs.BODY }}
steps:
- name: Checkout
uses: actions/checkout@v2
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/') }}
run: |
export LATEST_TAG=$(git describe --abbrev=0)
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
if [ "$IS_RELEASE" = true ]; then
export BODY=$(echo -e "$GIT_LOG")
else
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
if [ -z "$GIT_LOG" ]; then
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
else
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
fi
fi
echo -e "$BODY"
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
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@v2
with:
name: build-artifacts
path: CHANGELOG.txt
publish:
needs: changelog
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
@@ -88,16 +130,17 @@ jobs:
path: build-artifacts
- name: Publish Nightly [S3]
uses: kittaakos/upload-s3-action@v0.0.1
with:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_bucket: ${{ secrets.DOWNLOADS_BUCKET }}
source_dir: build-artifacts/
destination_dir: arduino-pro-ide/nightly/
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: "build-artifacts/*"
PLUGIN_STRIP_PREFIX: "build-artifacts/"
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 }}
release:
needs: build
needs: changelog
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
@@ -107,27 +150,27 @@ jobs:
name: build-artifacts
path: build-artifacts
- name: Create Release [GitHub]
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@v1-release
uses: svenstaro/upload-release-action@2.2.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
file: build-artifacts/*
tag: ${{ github.ref }}
file_glob: true
body: ${{ needs.changelog.outputs.BODY }}
- name: Publish Release [S3]
uses: kittaakos/upload-s3-action@v0.0.1
with:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_bucket: ${{ secrets.DOWNLOADS_BUCKET }}
source_dir: build-artifacts/
destination_dir: arduino-pro-ide/
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: "build-artifacts/*"
PLUGIN_STRIP_PREFIX: "build-artifacts/"
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 }}

122
.github/workflows/check-certificates.yml vendored Normal file
View File

@@ -0,0 +1,122 @@
name: Check for issues with signing certificates
on:
schedule:
# run every 10 hours
- cron: "0 */10 * * *"
# workflow_dispatch event allows the workflow to be triggered manually.
# This could be used to run an immediate check after updating certificate secrets.
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
workflow_dispatch:
env:
# Begin notifications when there are less than this many days remaining before expiration
EXPIRATION_WARNING_PERIOD: 30
jobs:
check-certificates:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
certificate:
- identifier: macOS signing certificate # Text used to identify the certificate in notifications
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password
- identifier: Windows signing certificate
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
steps:
- name: Set certificate path environment variable
run: |
# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
- name: Decode certificate
env:
CERTIFICATE: ${{ secrets[matrix.certificate.certificate-secret] }}
run: |
echo "${{ env.CERTIFICATE }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"
- name: Verify certificate
env:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
run: |
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-noout -passin env:CERTIFICATE_PASSWORD
) || (
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
exit 1
)
# See: https://github.com/rtCamp/action-slack-notify
- name: Slack notification of certificate verification failure
if: failure()
uses: rtCamp/action-slack-notify@v2.1.0
env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
SLACK_MESSAGE: |
:warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
:warning::warning::warning::warning:
SLACK_COLOR: danger
MSG_MINIMAL: true
- name: Get days remaining before certificate expiration date
env:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
id: get-days-before-expiration
run: |
EXPIRATION_DATE="$(
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
openssl x509 \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
# Display the expiration information in the log
echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
- name: Check if expiration notification period has been reached
id: check-expiration
run: |
if [[ ${{ steps.get-days-before-expiration.outputs.days }} -lt ${{ env.EXPIRATION_WARNING_PERIOD }} ]]; then
echo "::error::${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!"
exit 1
fi
- name: Slack notification of pending certificate expiration
# Don't send spurious expiration notification if verification fails
if: failure() && steps.check-expiration.outcome == 'failure'
uses: rtCamp/action-slack-notify@v2.1.0
env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
SLACK_MESSAGE: |
:warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
:warning::warning::warning::warning:
SLACK_COLOR: danger
MSG_MINIMAL: true

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules/
lib/
downloads/
build/
Examples/
!electron/build/
src-gen/
*webpack.config.js

View File

@@ -1,24 +0,0 @@
image:
file: Dockerfile
ports:
- port: 3000
onOpen: open-preview
- port: 5900
onOpen: ignore
- port: 6080
onOpen: ignore
tasks:
- init: >
yarn &&
yarn --cwd ./browser-app start
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addComment: false
addBadge: false

46
.vscode/tasks.json vendored
View File

@@ -4,7 +4,18 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino Pro IDE - Start Browser App",
"label": "Arduino IDE - Rebuild Electron App",
"type": "shell",
"command": "yarn rebuild:browser && yarn rebuild:electron",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
@@ -15,7 +26,7 @@
}
},
{
"label": "Arduino Pro IDE - Watch IDE Extension",
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -24,20 +35,9 @@
"panel": "new",
"clear": false
}
},
}
{
"label": "Arduino Pro IDE - Watch Debugger Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-debugger-extension watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Pro IDE - Watch Browser App",
"label": "Arduino IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
@@ -48,7 +48,7 @@
}
},
{
"label": "Arduino Pro IDE - Watch Electron App",
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
"command": "yarn --cwd ./electron-app watch",
"group": "build",
@@ -59,21 +59,19 @@
}
},
{
"label": "Arduino Pro IDE - Watch All [Browser]",
"label": "Arduino IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Debugger Extension",
"Arduino Pro IDE - Watch Browser App"
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Browser App"
]
},
{
"label": "Arduino Pro IDE - Watch All [Electron]",
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Debugger Extension",
"Arduino Pro IDE - Watch Electron App"
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Electron App"
]
}
]

View File

@@ -1,46 +1,50 @@
# Arduino Pro IDE
# Arduino IDE
[![Arduino Pro IDE](https://github.com/bcmi-labs/arduino-editor/workflows/Arduino%20Pro%20IDE/badge.svg)](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22)
[![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)
### Download
You can download the latest version of the Arduino Pro IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-pro-ide/releases) or following the links in the following table.
You can download the latest version of the Arduino IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-ide/releases) or following the links in the following table.
#### Latest version
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------ |
Linux | | [Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Windows 64 bit] |
macOS | | [macOS 64 bit] |
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------------------------------------------------------------ |
Linux | | [Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Windows 64 bit installer]<br />[Windows 64 bit MSI]<br />[Windows 64 bit ZIP] |
macOS | | [macOS 64 bit] |
[🚧 Work in progress...]: https://github.com/arduino/arduino-pro-ide/issues/287
[Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Linux_64bit.zip
[Windows 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Windows_64bit.zip
[macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_macOS_64bit.dmg
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
[Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Linux_64bit.zip
[Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.exe
[Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.msi
[Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.zip
[macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_macOS_64bit.dmg
#### Previous versions
These are available from the [GitHub releases page](https://github.com/arduino/arduino-pro-ide/releases).
These are available from the [GitHub releases page](https://github.com/arduino/arduino-ide/releases).
#### Nightly builds
These builds are generated every day at 03:00 GMT from the `master` branch and
These builds are generated every day at 03:00 GMT from the `main` branch and
should be considered unstable. In order to get the latest nightly build
available for the supported platform, use the following links:
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------ |
Linux | | [Nightly Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Nightly Windows 64 bit] |
macOS | | [Nightly macOS 64 bit] |
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
Linux | | [Nightly Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
macOS | | [Nightly macOS 64 bit] |
[🚧 Work in progress...]: https://github.com/arduino/arduino-pro-ide/issues/287
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_macOS_64bit.dmg
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, redirecting to latest
generated builds by replacing `latest` with the latest available build
@@ -50,7 +54,7 @@ macOS | | [Nightly macOS 64 bit] |
### Build from source
If youre familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
project, you should be able to build the Arduino Pro IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
### Build
```sh
@@ -71,8 +75,8 @@ yarn start
This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22).
- _Snapshot_ builds run when changes are pushed to the `master` branch, or when a PR is created against the `master` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
- _Nightly_ builds run every day at 03:00 GMT from the `master` branch.
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
- Create a local tag:
```sh
@@ -83,6 +87,38 @@ This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-e
git push origin 1.2.3
```
### Creating a GH release
This section guides you through how to create a new release. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
- Make sure the `main` state represents what you want to release and you're on `main`.
- Prepare a release-candidate build on a branch:
```bash
git branch 0.2.0-rc \
&& git checkout 0.2.0-rc
```
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
```bash
yarn update:version 0.2.0
```
- This should generate multiple outgoing changes with the version update.
- Commit your changes and push to the remote:
```bash
git add . \
&& git commit -s -m "Updated versions to 0.2.0" \
&& git push
```
- Create the GH PR the workflow starts automatically.
- Once you're happy with the RC, merge the changes to the `main`.
- Create a tag and push it:
```bash
git tag -a 0.2.0 -m "0.2.0" \
&& git push origin 0.2.0
```
- The release build starts automatically and uploads the artifacts with the changelog to the Pro IDE [release page](https://github.com/arduino/arduino-ide/releases).
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
- If you do not like the generated changelog, modify it and update the GH release.
### FAQ
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
@@ -90,9 +126,9 @@ This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-e
- Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?
- A: [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
- Windows: `C:\path\to\Arduino Pro IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino Pro IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino Pro IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
### Architecture overview

View File

@@ -1,30 +0,0 @@
{
"name": "arduino-debugger-extension",
"version": "0.1.0",
"description": "An extension for debugging Arduino programs",
"license": "MIT",
"dependencies": {
"@theia/debug": "next",
"arduino-ide-extension": "0.1.0",
"cdt-gdb-adapter": "^0.0.14",
"vscode-debugadapter": "^1.26.0",
"vscode-debugprotocol": "^1.26.0"
},
"scripts": {
"prepare": "yarn run clean && yarn run build",
"clean": "rimraf lib",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && yarn lint",
"watch": "tsc -w"
},
"files": [
"lib",
"src"
],
"theiaExtensions": [
{
"backend": "lib/node/arduino-debug-backend-module",
"frontend": "lib/browser/arduino-debug-frontend-module"
}
]
}

View File

@@ -1,39 +0,0 @@
import { DebugConfigurationManager } from "@theia/debug/lib/browser/debug-configuration-manager";
import { injectable } from "inversify";
@injectable()
export class ArduinoDebugConfigurationManager extends DebugConfigurationManager {
get defaultDebugger(): Promise<string | undefined> {
return this.debug.getDebuggersForLanguage('ino').then(debuggers => {
if (debuggers.length === 0)
return undefined;
return debuggers[0].type;
});
}
protected async selectDebugType(): Promise<string | undefined> {
const widget = this.editorManager.currentEditor;
if (!widget) {
return this.defaultDebugger;
}
const { languageId } = widget.editor.document;
const debuggers = await this.debug.getDebuggersForLanguage(languageId);
if (debuggers.length === 0) {
return this.defaultDebugger;
}
return this.quickPick.show(debuggers.map(
({ label, type }) => ({ label, value: type }),
{ placeholder: 'Select Environment' })
);
}
async createDefaultConfiguration(): Promise<void> {
const { model } = this;
if (model) {
await this.doCreate(model);
await this.updateModels();
}
}
}

View File

@@ -1,133 +0,0 @@
import { injectable, inject } from 'inversify';
import { MenuModelRegistry, Path, MessageService, Command, CommandRegistry } from '@theia/core';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { DebugFrontendApplicationContribution, DebugCommands } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FileSystem } from '@theia/filesystem/lib/common';
import URI from '@theia/core/lib/common/uri';
import { EditorManager } from '@theia/editor/lib/browser';
import { EditorMode } from "arduino-ide-extension/lib/browser/editor-mode";
import { SketchesService } from 'arduino-ide-extension/lib/common/protocol/sketches-service';
import { ArduinoToolbar } from 'arduino-ide-extension/lib/browser/toolbar/arduino-toolbar';
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
export namespace ArduinoDebugCommands {
export const START_DEBUG: Command = {
id: 'arduino-start-debug',
label: 'Start Debugging'
}
}
@injectable()
export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendApplicationContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(MessageService)
protected readonly messageService: MessageService;
async start(noDebug?: boolean, debugSessionOptions?: DebugSessionOptions): Promise<void> {
const configurations = this.configurations as ArduinoDebugConfigurationManager;
let current = debugSessionOptions ? debugSessionOptions : configurations.current;
// If no configurations are currently present, create them
if (!current) {
await configurations.createDefaultConfiguration();
current = configurations.current;
}
if (current) {
if (noDebug !== undefined) {
current = {
...current,
configuration: {
...current.configuration,
noDebug
}
};
}
if (current.configuration.type === 'arduino') {
const wsStat = this.workspaceService.workspace;
let sketchFileURI: URI | undefined;
if (wsStat && await this.sketchesService.isSketchFolder(wsStat.uri)) {
const wsPath = new Path(wsStat.uri);
const sketchFilePath = wsPath.join(wsPath.name + '.ino').toString();
sketchFileURI = new URI(sketchFilePath);
} else if (this.editorManager.currentEditor) {
const editorURI = this.editorManager.currentEditor.getResourceUri();
if (editorURI && editorURI.path && editorURI.path.ext === '.ino') {
sketchFileURI = editorURI;
}
}
if (sketchFileURI) {
await this.editorManager.open(sketchFileURI);
await this.manager.start(current);
} else {
this.messageService.error('Please open a sketch file to start debugging.')
}
} else {
await this.manager.start(current);
}
}
}
initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
return Promise.resolve();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.editorMode.proMode) {
super.registerMenus(menus);
menus.unregisterMenuAction(DebugCommands.START_NO_DEBUG);
}
}
registerKeybindings(keybindings: KeybindingRegistry): void {
if (this.editorMode.proMode) {
super.registerKeybindings(keybindings);
keybindings.unregisterKeybinding({
command: DebugCommands.START_NO_DEBUG.id,
keybinding: 'ctrl+f5'
});
}
}
registerToolbarItems(toolbar: TabBarToolbarRegistry): void {
super.registerToolbarItems(toolbar);
toolbar.registerItem({
id: ArduinoDebugCommands.START_DEBUG.id,
command: ArduinoDebugCommands.START_DEBUG.id,
tooltip: 'Start Debugging',
priority: 3
});
}
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(ArduinoDebugCommands.START_DEBUG, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => {
registry.executeCommand(DebugCommands.START.id);
}
});
}
}

View File

@@ -1,19 +0,0 @@
import { ContainerModule } from 'inversify';
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
import { ArduinoVariableResolver } from './arduino-variable-resolver';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
import { ArduinoDebugFrontendApplicationContribution } from './arduino-debug-frontend-application-contribution';
import { ArduinoDebugSessionManager } from './arduino-debug-session-manager';
import '../../src/browser/style/index.css';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoVariableResolver).toSelf().inSingletonScope();
bind(VariableContribution).toService(ArduinoVariableResolver);
rebind(DebugSessionManager).to(ArduinoDebugSessionManager).inSingletonScope();
rebind(DebugConfigurationManager).to(ArduinoDebugConfigurationManager).inSingletonScope();
rebind(DebugFrontendApplicationContribution).to(ArduinoDebugFrontendApplicationContribution);
});

View File

@@ -1,14 +0,0 @@
import { DebugSessionManager } from "@theia/debug/lib/browser/debug-session-manager";
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
export class ArduinoDebugSessionManager extends DebugSessionManager {
start(options: DebugSessionOptions) {
if (options.configuration.type === 'arduino' && this.sessions.find(s => s.configuration.type === 'arduino')) {
this.messageService.info('A debug session is already running. You must stop the running session before starting a new one.')
return Promise.resolve(undefined);
}
return super.start(options);
}
}

View File

@@ -1,46 +0,0 @@
import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser';
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { BoardsServiceClientImpl } from 'arduino-ide-extension/lib/browser/boards/boards-service-client-impl';
@injectable()
export class ArduinoVariableResolver implements VariableContribution {
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(MessageService)
protected readonly messageService: MessageService
registerVariables(variables: VariableRegistry): void {
variables.registerVariable(<Variable>{
name: 'fqbn',
description: 'Qualified name of the selected board',
resolve: this.resolveFqbn.bind(this),
});
variables.registerVariable({
name: 'port',
description: 'Selected upload port',
resolve: this.resolvePort.bind(this)
});
}
protected async resolveFqbn(): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
this.messageService.error('No board selected. Please select a board for debugging.');
return undefined;
}
return boardsConfig.selectedBoard.fqbn;
}
protected async resolvePort(): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedPort) {
return undefined;
}
return boardsConfig.selectedPort.address;
}
}

View File

@@ -1,16 +0,0 @@
.arduino-start-debug-icon {
-webkit-mask: url('debug-dark.svg') 50%;
mask: url('debug-dark.svg') 50%;
-webkit-mask-size: 100%;
mask-size: 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-ui-button-font-color);
}
.arduino-start-debug {
border-radius: 12px;
}

View File

@@ -1,89 +0,0 @@
import * as path from 'path';
import { injectable, inject } from 'inversify';
import { DebugAdapterContribution, DebugAdapterExecutable } from '@theia/debug/lib/common/debug-model';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import { ArduinoDaemonImpl } from 'arduino-ide-extension/lib/node/arduino-daemon-impl';
@injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
readonly type = 'arduino';
readonly label = 'Arduino';
readonly languages = ['c', 'cpp', 'ino'];
@inject(ArduinoDaemonImpl) daemon: ArduinoDaemonImpl;
getSchemaAttributes(): IJSONSchema[] {
return [
{
'properties': {
'sketch': {
'type': 'string',
'description': 'path to the sketch root ino file',
'default': '${file}',
},
'pauseAtMain': {
'description': 'If enabled the debugger will pause at the start of the main function.',
'type': 'boolean',
'default': false
},
'debugDebugAdapter': {
'type': 'boolean',
'description': 'Start the debug adapter in debug mode (with --inspect-brk)',
'default': false
},
}
}
]
}
provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable {
const debugAdapterMain = path.join(__dirname, 'debug-adapter', 'main');
if (config.debugDebugAdapter) {
return {
command: process.execPath,
args: ['--inspect-brk', debugAdapterMain],
}
}
return {
modulePath: debugAdapterMain,
args: [],
}
}
provideDebugConfigurations(): DebugConfiguration[] {
return [
<DebugConfiguration>{
name: this.label,
type: this.type,
request: 'launch'
}
];
}
async resolveDebugConfiguration(config: DebugConfiguration): Promise<DebugConfiguration> {
const startFunction = config.pauseAtMain ? 'main' : 'setup';
const res: ActualDebugConfig = {
...config,
arduinoCli: await this.daemon.getExecPath(),
fqbn: '${fqbn}',
uploadPort: '${port}',
initCommands: [
`-break-insert -t --function ${startFunction}`
]
}
if (!res.sketch) {
res.sketch = '${file}';
}
return res;
}
}
interface ActualDebugConfig extends DebugConfiguration {
arduinoCli?: string;
sketch?: string;
fqbn?: string;
uploadPort?: string;
}

View File

@@ -1,7 +0,0 @@
import { ContainerModule } from 'inversify';
import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model';
import { ArduinoDebugAdapterContribution } from './arduino-debug-adapter-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugAdapterContribution).to(ArduinoDebugAdapterContribution).inSingletonScope();
});

View File

@@ -1,140 +0,0 @@
import { DebugProtocol } from 'vscode-debugprotocol';
import { GDBDebugSession, FrameVariableReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import * as mi from 'cdt-gdb-adapter/dist/mi';
import { ArduinoGDBBackend } from './arduino-gdb-backend';
import { ArduinoVariableHandler } from './arduino-variable-handler';
import { Scope, OutputEvent } from 'vscode-debugadapter';
export interface ArduinoLaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
arduinoCli?: string;
sketch?: string;
fqbn?: string;
uploadPort?: string;
}
const GLOBAL_HANDLE_ID = 0xFE;
const STATIC_HANDLES_START = 0x010000;
const STATIC_HANDLES_FINISH = 0x01FFFF;
export class ArduinoDebugSession extends GDBDebugSession {
private _variableHandler: ArduinoVariableHandler;
get arduinoBackend(): ArduinoGDBBackend {
return this.gdb as ArduinoGDBBackend;
}
protected get variableHandler() {
if (this._variableHandler) {
return this._variableHandler;
}
if (!this.gdb) {
throw new Error("GDB backend is not ready.");
}
const handler = new ArduinoVariableHandler(this, this.frameHandles, this.variableHandles);
this._variableHandler = handler;
return handler;
}
protected createBackend(): GDBBackend {
return new ArduinoGDBBackend();
}
protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): Promise<void> {
try {
await this.gdb.sendCommand('-interpreter-exec console "monitor reset halt"')
await mi.sendExecContinue(this.gdb);
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 100, err.message);
}
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): Promise<void> {
if (process.platform === 'win32') {
const message = 'Pause is not supported on Windows. Please stop the debug session and set a breakpoint instead.';
this.sendEvent(new OutputEvent(message));
this.sendErrorResponse(response, 1, message);
return Promise.resolve();
}
return super.pauseRequest(response, args);
}
protected async disconnectRequest(response: DebugProtocol.DisconnectResponse): Promise<void> {
try {
if (this.isRunning) {
if (process.platform === 'win32') {
// We cannot pause on Windows
this.arduinoBackend.kill();
this.sendResponse(response);
return;
}
// Need to pause first
const waitPromise = new Promise(resolve => this.waitPaused = resolve);
this.gdb.pause();
await waitPromise;
}
await this.gdb.sendGDBExit();
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
try {
const frame: FrameVariableReference = {
type: 'frame',
frameHandle: args.frameId,
};
// const pins: ObjectVariableReference = {
// type: "object",
// varobjName: "__pins",
// frameHandle: 42000,
// }
response.body = {
scopes: [
// new Scope('Pins', this.variableHandles.create(pins), false),
new Scope('Local', this.variableHandles.create(frame), false),
new Scope('Global', GLOBAL_HANDLE_ID, false),
// new Scope('Static', STATIC_HANDLES_START + parseInt(args.frameId as any, 10), false)
],
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> {
try {
response.body = {
variables: [] as DebugProtocol.Variable[]
};
const ref = this.variableHandles.get(args.variablesReference);
if (args.variablesReference === GLOBAL_HANDLE_ID) {
// Use hardcoded global handle to load and store global variables
response.body.variables = await this.variableHandler.getGlobalVariables();
} else if (args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_FINISH) {
// Use STATIC_HANDLES_START to shift the framehandles back
const frameHandle = args.variablesReference - STATIC_HANDLES_START;
response.body.variables = await this.variableHandler.getStaticVariables(frameHandle);
} else if (ref && ref.type === 'frame') {
// List variables for current frame
response.body.variables = await this.handleVariableRequestFrame(ref);
} else if (ref && ref.varobjName === '__pins') {
response.body.variables = await this.variableHandler.handlePinStatusRequest();
} else if (ref && ref.type === 'object') {
// List data under any variable
response.body.variables = await this.handleVariableRequestObject(ref);
}
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
}

View File

@@ -1,72 +0,0 @@
import * as path from 'path';
import * as fs from 'arduino-ide-extension/lib/node/fs-extra'
import { spawn } from 'child_process';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import { MIFrameInfo } from 'cdt-gdb-adapter/dist/mi';
import { ArduinoLaunchRequestArguments } from './arduino-debug-session';
import { ArduinoParser } from './arduino-parser';
export class ArduinoGDBBackend extends GDBBackend {
constructor() {
super();
this.parser = new ArduinoParser(this);
}
spawn(requestArgs: ArduinoLaunchRequestArguments): Promise<void> {
if (!requestArgs.sketch) {
throw new Error('Missing argument: sketch');
}
if (!requestArgs.fqbn) {
throw new Error('Missing argument: fqbn')
}
const sketchFS = fs.statSync(requestArgs.sketch);
const sketchDir = sketchFS.isFile() ? path.dirname(requestArgs.sketch) : requestArgs.sketch;
const command = requestArgs.arduinoCli || 'arduino-cli';
const args = [
'debug',
'-p', requestArgs.uploadPort || 'none',
'-b', requestArgs.fqbn,
'--interpreter', 'mi2',
sketchDir
];
const proc = spawn(command, args);
this.proc = proc;
this.out = proc.stdin;
return (this.parser as ArduinoParser).parseFull(proc);
}
sendFileExecAndSymbols(): Promise<void> {
// The program file is already sent by `arduino-cli`
return Promise.resolve();
}
sendExecInterrupt(threadId?: number) {
let command = '-exec-interrupt';
if (threadId) {
command += ` --thread ${threadId}`;
}
return this.sendCommand(command);
}
sendStackInfoFrame(threadId: number, frameId: number): Promise<{ frame: MIFrameInfo }> {
const command = `-stack-info-frame --thread ${threadId} --frame ${frameId}`;
return this.sendCommand(command);
}
sendTargetDetach(): Promise<void> {
return this.sendCommand('-target-detach');
}
kill(): void {
if (!this.proc) {
return;
}
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', this.proc.pid.toString(), '/f', '/t']);
} else {
this.proc.kill('SIGKILL');
}
}
}

View File

@@ -1,76 +0,0 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { Readable } from 'stream';
import { MIParser } from "cdt-gdb-adapter/dist/MIParser";
const LINE_REGEX = /(.*)(\r?\n)/;
export class ArduinoParser extends MIParser {
protected rejectReady?: (error: Error) => void;
parseFull(proc: ChildProcessWithoutNullStreams): Promise<void> {
return new Promise((resolve, reject) => {
// Detect errors when the child process could not be spawned
proc.on('error', reject);
this.waitReady = () => {
this.rejectReady = undefined;
resolve();
}
this.rejectReady = (error: Error) => {
this.waitReady = undefined;
reject(error);
}
// Detect unexpected termination
proc.on('exit', () => {
if (this.rejectReady) {
this.rejectReady(new Error('The gdb debugger terminated unexpectedly.'));
}
});
this.readInputStream(proc.stdout);
this.readErrorStream(proc.stderr);
});
}
private readInputStream(stream: Readable) {
let buff = '';
stream.on('data', chunk => {
buff += chunk.toString();
let regexArray = LINE_REGEX.exec(buff);
while (regexArray) {
const line = regexArray[1];
this.line = line;
this.pos = 0;
this.handleLine();
// Detect error emitted as log message
if (this.rejectReady && line.toLowerCase().startsWith('&"error')) {
this.pos = 1;
this.rejectReady(new Error(this.handleCString() || regexArray[1]));
this.rejectReady = undefined;
}
buff = buff.substring(regexArray[1].length + regexArray[2].length);
regexArray = LINE_REGEX.exec(buff);
}
});
}
private readErrorStream(stream: Readable) {
let buff = '';
stream.on('data', chunk => {
buff += chunk.toString();
let regexArray = LINE_REGEX.exec(buff);
while (regexArray) {
const line = regexArray[1];
this.gdb.emit('consoleStreamOutput', line + '\n', 'stderr');
// Detect error emitted on the stderr stream
if (this.rejectReady && line.toLowerCase().startsWith('error')) {
this.rejectReady(new Error(line));
this.rejectReady = undefined;
}
buff = buff.substring(regexArray[1].length + regexArray[2].length);
regexArray = LINE_REGEX.exec(buff);
}
});
}
}

View File

@@ -1,115 +0,0 @@
import * as path from 'path';
import { DebugProtocol } from "vscode-debugprotocol";
import { Handles } from 'vscode-debugadapter/lib/handles';
import { FrameReference, VariableReference } from "cdt-gdb-adapter/dist/GDBDebugSession";
import { VarManager } from 'cdt-gdb-adapter/dist/varManager';
import * as mi from 'cdt-gdb-adapter/dist/mi';
import { ArduinoDebugSession } from "./arduino-debug-session";
import { ArduinoGDBBackend } from './arduino-gdb-backend';
export class ArduinoVariableHandler {
protected readonly gdb: ArduinoGDBBackend;
protected readonly varMgr: VarManager;
protected globalHandle: number;
constructor(protected readonly session: ArduinoDebugSession,
protected frameHandles: Handles<FrameReference>,
protected variableHandles: Handles<VariableReference>) {
this.gdb = session.arduinoBackend;
this.varMgr = new VarManager(this.gdb);
}
createGlobalHandle() {
this.globalHandle = this.frameHandles.create({
threadId: -1,
frameId: -1
});
}
/** TODO */
async getGlobalVariables(): Promise<DebugProtocol.Variable[]> {
throw new Error('Global variables are not supported yet.');
const frame = this.frameHandles.get(this.globalHandle);
const symbolInfo: any[] = [] // this.symbolTable.getGlobalVariables();
const variables: DebugProtocol.Variable[] = [];
for (const symbol of symbolInfo) {
const name = `global_var_${symbol.name}`;
const variable = await this.getVariables(frame, name, symbol.name, -1);
variables.push(variable);
}
return variables;
}
/** TODO */
async getStaticVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
throw new Error('Static variables are not supported yet.');
const frame = this.frameHandles.get(frameHandle);
const result = await this.gdb.sendStackInfoFrame(frame.threadId, frame.frameId);
const file = path.normalize(result.frame.file || '');
const symbolInfo: any[] = [] // this.symbolTable.getStaticVariables(file);
const variables: DebugProtocol.Variable[] = [];
// Fetch stack depth to obtain frameId/threadId/depth tuple
const stackDepth = await mi.sendStackInfoDepth(this.gdb, { maxDepth: 100 });
const depth = parseInt(stackDepth.depth, 10);
for (const symbol of symbolInfo) {
const name = `${file}_static_var_${symbol.name}`;
const variable = await this.getVariables(frame, name, symbol.name, depth);
variables.push(variable);
}
return variables;
}
private async getVariables(frame: FrameReference, name: string, expression: string, depth: number): Promise<DebugProtocol.Variable> {
let global = this.varMgr.getVar(frame.frameId, frame.threadId, depth, name);
if (global) {
// Update value if it is already loaded
const vup = await mi.sendVarUpdate(this.gdb, { name });
const update = vup.changelist[0];
if (update && update.in_scope === 'true' && update.name === global.varname) {
global.value = update.value;
}
} else {
// create var in GDB and store it in the varMgr
const varCreateResponse = await mi.sendVarCreate(this.gdb, {
name,
frame: 'current',
expression,
});
global = this.varMgr.addVar(frame.frameId, frame.threadId, depth, name, true, false, varCreateResponse);
}
return {
name: expression,
value: (global.value === void 0) ? '<unknown>' : global.value,
type: global.type,
variablesReference: parseInt(global.numchild, 10) > 0
? this.variableHandles.create({
frameHandle: this.globalHandle,
type: 'object',
varobjName: global.varname,
})
: 0,
};
}
async handlePinStatusRequest(): Promise<DebugProtocol.Variable[]> {
const variables: DebugProtocol.Variable[] = [];
variables.push({
name: "D2",
type: "gpio",
value: "0x00",
variablesReference: 0
})
return variables;
}
}

View File

@@ -1,10 +0,0 @@
import * as process from 'process';
import { logger } from 'vscode-debugadapter/lib/logger';
import { ArduinoDebugSession } from './arduino-debug-session';
import { DebugSession } from 'vscode-debugadapter';
process.on('uncaughtException', (err: any) => {
logger.error(JSON.stringify(err));
});
DebugSession.run(ArduinoDebugSession);

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"noImplicitAny": true,
"noEmitOnError": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"outDir": "lib",
"lib": [
"es6",
"dom"
],
"jsx": "react",
"sourceMap": true,
"skipLibCheck": true
},
"include": [
"src"
],
"files": [
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
]
}

View File

@@ -1,37 +0,0 @@
{
"rules": {
"class-name": true,
"comment-format": [true, "check-space"],
"curly": false,
"forin": false,
"indent": [true, "spaces"],
"max-line-length": [true, 180],
"no-trailing-whitespace": false,
"no-unused-expression": true,
"no-var-keyword": true,
"one-line": [true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"radix": true,
"trailing-comma": [false],
"triple-equals": [true, "allow-null-check"],
"typedef-whitespace": [true, {
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}],
"variable-name": false,
"whitespace": [true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

View File

@@ -1,6 +1,6 @@
## Arduino IDE Extension
Arduino Pro IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
Arduino IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
### IDE Services
@@ -50,3 +50,7 @@ The Config Service knows about your system, like for example the default sketch
- [src/node/config-service-impl.ts](./src/node/config-service-impl.ts) implements the service backend:
- getting the `arduino-cli` version and configuration
- 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.

View File

@@ -1,117 +0,0 @@
{
"$id": "http://arduino.cc/arduino-cli.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Arduino CLI Configuration",
"properties": {
"board_manager": {
"type": "object",
"description": "Board Manager Configuration",
"properties": {
"additional_urls": {
"type": "array",
"description": "If your board requires 3rd party core packages to work, you can list the URLs to additional package indexes in the Arduino CLI configuration file.",
"items": {
"type": "string",
"description": "URL pointing to the 3rd party core package index JSON.",
"pattern": "^(.*)$"
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"daemon": {
"type": "object",
"description": "CLI Daemon Configuration",
"properties": {
"port": {
"type": [
"string",
"number"
],
"description": "The CLI daemon port where the gRPC clients can connect to.",
"pattern": "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
"additionalProperties": false
}
},
"additionalProperties": false
},
"directories": {
"type": "object",
"description": "Directories Configuration",
"properties": {
"data": {
"type": "string",
"description": "Path to the the data folder where core packages will be stored.",
"pattern": "^(.*)$"
},
"downloads": {
"type": "string",
"description": "Path to the staging folder.",
"pattern": "^(.*)$"
},
"user": {
"type": "string",
"description": "Path to the sketchbooks.",
"pattern": "^(.*)$"
}
},
"additionalProperties": false
},
"logging": {
"type": "object",
"description": "Logging Configuration",
"properties": {
"file": {
"type": "string",
"description": "Path to the file where logs will be written.",
"pattern": "^(.*)$"
},
"format": {
"type": "string",
"description": "The output format for the logs, can be 'text' or 'json'",
"enum": [
"text",
"json"
]
},
"level": {
"type": "string",
"description": "Messages with this level and above will be logged.",
"enum": [
"trace",
"debug",
"info",
"warning",
"error",
"fatal",
"panic"
]
}
},
"additionalProperties": false
},
"telemetry": {
"type": "object",
"description": "Telemetry Configuration",
"properties": {
"addr": {
"type": "string",
"description": "Address to the telemetry endpoint. Must be a full address with host, address, and port. For instance, ':9090' represents 'localhost:9090'",
"pattern": "^(.*)$"
},
"enabled": {
"type": "boolean",
"description": "Whether the telemetry is enabled or not."
},
"additionalProperties": false
},
"additionalProperties": false
}
},
"// TODOs": [
"additionalProperties should be true. See the new telemetry entry"
],
"additionalProperties": false
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
{
"name": "arduino-ide-extension",
"version": "0.1.0",
"version": "2.0.0-beta.1",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"scripts": {
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build",
"clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js",
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
@@ -22,7 +23,6 @@
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
@@ -41,6 +41,7 @@
"@types/ncp": "^2.0.4",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
@@ -56,8 +57,10 @@
"ncp": "^2.0.0",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-disable": "^0.1.0",
"react-select": "^3.0.4",
"semver": "^6.3.0",
"react-tabs": "^3.1.2",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"tree-kill": "^1.2.1",
@@ -99,7 +102,8 @@
"lib",
"src",
"build",
"data"
"data",
"examples"
],
"theiaExtensions": [
{
@@ -112,6 +116,14 @@
},
{
"frontend": "lib/browser/boards/quick-open/boards-quick-open-module"
},
{
"electronMain": "lib/electron-main/arduino-electron-main-module"
}
]
],
"arduino": {
"cli": {
"version": "20210212"
}
}
}

View File

@@ -1,62 +1,141 @@
// @ts-check
// The links to the downloads as of today (02.09.) are the followings:
// In order to get the latest nightly build for your platform use the following links replacing <DATE> with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 )
// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_64bit.tar.gz
// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_ARM64.tar.gz
// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Windows_64bit.zip
// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_macOS_64bit.tar.gz
// [...]
// redirecting to latest generated builds by replacing latest with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 latest is replaced with 20190806
(() => {
const DEFAULT_VERSION = '0.12.0'; // require('moment')().format('YYYYMMDD');
(async () => {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
const semver = require('semver');
const moment = require('moment');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
describe: `The version of the 'arduino-cli' to download, or 'nightly-latest'. Defaults to ${DEFAULT_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-cli' even if it already exists on the file system.`
})
.version(false).parse();
const version = yargs['cli-version'];
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm': return 'Linux_ARMv7.tar.gz';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
default: return undefined;
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
return undefined;
}
const { arduino } = pkg;
if (!arduino) {
return undefined;
}
const { cli } = arduino;
if (!cli) {
return undefined;
}
const { version } = cli;
return version;
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
if (!version) {
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
shell.exit(1);
}
const url = `https://downloads.arduino.cc/arduino-cli${version.startsWith('nightly-') ? '/nightly' : ''}/arduino-cli_${version}_${suffix}`;
downloader.downloadUnzipFile(url, cli, 'arduino-cli', force);
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
const destinationPath = path.join(buildFolder, cliName);
if (typeof version === 'string') {
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm': return 'Linux_ARMv7.tar.gz';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
default: return undefined;
}
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
shell.echo(`📦 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(`🌙 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);
}
} 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 CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
if (fs.existsSync(destinationPath)) {
shell.echo(`Skipping the CLI 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 CLI source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned CLI 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 CLI...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< CLI build done.')
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
shell.exit(1);
}
const builtCliPath = path.join(tempRepoPath, cliName);
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the CLI.`);
shell.echo('<<< Verifying CLI...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified CLI.');
}
})();

View File

@@ -0,0 +1,33 @@
// @ts-check
// The version to use.
const version = '1.9.0';
(async () => {
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
process.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
shell.exit(1);
process.exit(1);
}
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
process.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
})();

View File

@@ -6,7 +6,7 @@
(() => {
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = '9.0.0';
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
const path = require('path');
const shell = require('shelljs');
@@ -22,7 +22,7 @@
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['8.0.1', '9.0.0'],
choices: ['snapshot_20210124'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
@@ -38,35 +38,35 @@
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const alsTarget = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
let clangdTarget, alsSuffix, clangdSuffix;
let clangdExecutablePath, lsSuffix, clangdPrefix;
switch (platform) {
case 'darwin':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'macOS_amd64.zip';
clangdPrefix = 'mac';
break;
case 'linux':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Linux_amd64.zip';
clangdSuffix = 'linux.zip'
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'Linux_amd64.zip';
clangdPrefix = 'linux'
break;
case 'win32':
clangdTarget = path.join(build, 'clangd.exe')
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
lsSuffix = 'Windows_amd64.zip';
clangdPrefix = 'windows';
break;
}
if (!alsSuffix) {
if (!lsSuffix) {
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${alsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, alsTarget, force);
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.downloadUnzipAll(clangdUrl, build, clangdTarget, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`;
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.
})();

View File

@@ -21,9 +21,9 @@ process.on('uncaughtException', error => {
* @param url {string} Download URL
* @param targetFile {string} Path to the file to copy from the decompressed archive
* @param filePrefix {string} Prefix of the file name found in the archive
* @param force {boolean} Whether to download even if the target file exists
* @param force {boolean} Whether to download even if the target file exists. `false` by default.
*/
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
@@ -79,7 +79,7 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressOptions = undefined) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
@@ -96,12 +96,16 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, targetDir, {
let options = {
plugins: [
unzip(),
untargz()
]
});
};
if (decompressOptions) {
options = Object.assign(options, decompressOptions)
}
const files = await decompress(data, targetDir, options);
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);

View File

@@ -16,23 +16,98 @@
shell.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino-cli.git ${repository}`).code !== 0) {
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);
}
shell.echo(`<<< Repository cloned.`);
const { platform } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const rawVersion = shell.exec(`${cli} version`).trim();
if (!rawVersion) {
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 version = rawVersion.substring(rawVersion.lastIndexOf('Commit:') + 'Commit:'.length).trim();
if (version) {
if (shell.exec(`git -C ${repository} checkout ${version} -b ${version}`).code !== 0) {
// 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",
"Commit": "079bb6c6",
"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}'.`);
} 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}'.`);
} else {
shell.echo(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
}
shell.echo('>>> Generating TS/JS API from:');

View File

@@ -20,11 +20,4 @@ export namespace ArduinoCommands {
id: 'arduino-open-boards-dialog'
};
export const TOGGLE_ADVANCED_MODE: Command = {
id: 'arduino-toggle-advanced-mode'
};
export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = {
id: 'arduino-toggle-advanced-mode-toolbar'
};
}

View File

@@ -1,53 +0,0 @@
import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ArduinoDaemonClient } from '../common/protocol';
@injectable()
export class ArduinoDaemonClientImpl implements ArduinoDaemonClient {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
protected readonly onStartedEmitter = new Emitter<void>();
protected readonly onStoppedEmitter = new Emitter<void>();
protected _isRunning = false;
notifyStopped(): void {
if (this._isRunning) {
this._isRunning = false;
this.onStoppedEmitter.fire();
this.info('The CLI daemon process has stopped.');
}
}
notifyStarted(): void {
if (!this._isRunning) {
this._isRunning = true;
this.onStartedEmitter.fire();
this.info('The CLI daemon process has started.');
}
}
get onDaemonStarted(): Event<void> {
return this.onStartedEmitter.event;
}
get onDaemonStopped(): Event<void> {
return this.onStoppedEmitter.event;
}
get isRunning(): boolean {
return this._isRunning;
}
protected info(message: string): void {
this.messageService.info(message, { timeout: 3000 });
this.logger.info(message);
}
}

View File

@@ -1,4 +1,5 @@
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core';
const debounce = require('lodash.debounce');
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } from '@theia/core';
import {
ContextMenuRenderer,
FrontendApplication, FrontendApplicationContribution,
@@ -13,7 +14,6 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu, EditorManager } from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem } from '@theia/filesystem/lib/common';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
@@ -24,8 +24,9 @@ import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspac
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, BoardsServiceClient, CoreService, Port, SketchesService, ToolOutputServiceClient } from '../common/protocol';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
@@ -33,7 +34,7 @@ import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsDataStore } from './boards/boards-data-store';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { EditorMode } from './editor-mode';
import { ArduinoMenus } from './menu/arduino-menus';
@@ -41,11 +42,20 @@ import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { OutputService } from '../common/protocol/output-service';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
@injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
@inject(ILogger)
protected logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
@@ -55,15 +65,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(ToolOutputServiceClient)
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
// Unused but do not remove it. It's required by DI, otherwise `init` method is not called.
@inject(BoardsServiceClient)
protected readonly boardsServiceClient: BoardsServiceClient;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@@ -77,8 +80,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(FileService)
protected readonly fileSystem: FileService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@@ -140,6 +143,23 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(ExecutableService)
protected executableService: ExecutableService;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
@@ -177,6 +197,61 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
viewContribution.initializeLayout(app);
}
}
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
await this.hostedPluginSupport.didStart;
this.startLanguageServer(fqbn, name);
}
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
}
protected startLanguageServer = debounce((fqbn: string, name: string | undefined) => this.doStartLanguageServer(fqbn, name));
protected async doStartLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileSystem.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath] = await Promise.all([
this.fileSystem.fsPath(new URI(clangdUri)),
this.fileSystem.fsPath(new URI(cliUri)),
this.fileSystem.fsPath(new URI(lsUri)),
]);
this.commandRegistry.executeCommand('arduino.languageserver.start', {
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
board: {
fqbn,
name: name ? `"${name}"` : undefined
}
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -194,12 +269,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor'
});
registry.registerItem({
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id,
tooltip: this.editorMode.proMode ? 'Switch to Classic Mode' : 'Switch to Advanced Mode',
text: this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)'
});
}
registerCommands(registry: CommandRegistry): void {
@@ -208,62 +277,56 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: string) => {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
}
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
}
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
isToggled: () => this.editorMode.proMode,
execute: () => this.editorMode.toggleProMode()
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => this.editorMode.proMode,
execute: () => this.editorMode.toggleProMode()
});
}
registerMenus(registry: MenuModelRegistry) {
if (!this.editorMode.proMode) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '1'
});
registry.registerMenuAction(CommonMenus.HELP, {
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
label: 'Advanced Mode'
order: '4'
});
}
protected async openSketchFiles(uri: string): Promise<void> {
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri);
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
for (const uri of [mainFileUri, ...otherSketchFileUris, ...additionalFileUris]) {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
@@ -303,7 +366,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
light: 'editorWidget.background',
hc: 'editorWidget.background'
},
description: 'Color of the Arduino Pro IDE foreground which is used for dialogs, such as the Select Board dialog.'
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',

View File

@@ -6,23 +6,16 @@ import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contrib
import { TabBarToolbarContribution, TabBarToolbarFactory } 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 { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { 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';
@@ -40,7 +33,9 @@ import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry
KeybindingRegistry as TheiaKeybindingRegistry,
TabBarRendererFactory,
ContextMenuRenderer
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import { ApplicationShell } from './theia/core/application-shell';
@@ -54,7 +49,7 @@ import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspa
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
import { ConfigService, ConfigServicePath, ConfigServiceClient } from '../common/protocol/config-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
@@ -64,24 +59,19 @@ import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
import { ProblemManager } from './theia/markers/problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { AboutDialog } from './theia/core/about-dialog';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { ArduinoDaemonClientImpl } from './arduino-daemon-client-impl';
import { ArduinoDaemonClient, ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser';
import { ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { EditorManager as TheiaEditorManager, EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
import { EditorManager } from './theia/editor/editor-manager';
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution
} from '@theia/core/lib/browser/connection-status-service';
import { ConfigServiceClientImpl } from './config-service-client-impl';
import { CoreServiceClientImpl } from './core-service-client-impl';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
@@ -95,7 +85,7 @@ import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './th
import { Contribution } from './contributions/contribution';
import { NewSketch } from './contributions/new-sketch';
import { OpenSketch } from './contributions/open-sketch';
import { CloseSketch } from './contributions/close-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';
@@ -117,11 +107,46 @@ import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/l
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget';
import { BurnBootloader } from './contributions/burn-bootloader';
import { ExamplesServicePath, ExamplesService } from '../common/protocol/examples-service';
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
import { IncludeLibrary } from './contributions/include-library';
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputChannelManager } from './theia/output/output-channel';
import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, OutputChannelRegistryMainImpl } from './theia/plugin-ext/output-channel-registry-main';
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { OutputServiceImpl } from './output-service-impl';
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
import { NotificationCenter } from './notification-center';
import { NotificationServicePath, NotificationServiceServer } from '../common/protocol';
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 { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { BoardSelection } from './contributions/board-selection';
import { OpenRecentSketch } from './contributions/open-recent-sketch';
import { Help } from './contributions/help';
import { bindArduinoPreferences } from './arduino-preferences'
import { SettingsService, SettingsDialog, SettingsWidget, SettingsDialogProps } from './settings';
import { AddFile } from './contributions/add-file';
import { ArchiveSketch } from './contributions/archive-sketch';
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { OutputToolbarContribution } from './theia/output/output-toolbar-contribution';
const ElementQueries = require('css-element-queries/src/ElementQueries');
MonacoThemingService.register({
id: 'arduinoTheme',
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/arduino.color-theme.json')
@@ -142,15 +167,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
// `ino` TextMate grammar and language client
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
// Renderer for both the library and the core widgets.
bind(ListItemRenderer).toSelf().inSingletonScope();
// Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
// Library list widget
bind(LibraryListWidget).toSelf();
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
@@ -163,36 +185,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SketchesServiceClientImpl);
// Config service
bind(ConfigService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(ConfigServiceClientImpl);
return connection.createProxy(ConfigServicePath, client);
}).inSingletonScope();
bind(ConfigServiceClientImpl).toSelf().inSingletonScope();
bind(ConfigServiceClient).toDynamicValue(context => {
const client = context.container.get(ConfigServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath, client);
return client;
}).inSingletonScope();
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(BoardsServiceClientImpl);
return connection.createProxy(BoardsServicePath, client);
}).inSingletonScope();
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
bind(BoardsServiceClient).toDynamicValue(async context => {
const client = context.container.get(BoardsServiceClientImpl);
const service = context.container.get<BoardsService>(BoardsService);
await client.init(service);
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
// 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();
@@ -225,26 +227,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
// Core service
bind(CoreService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(CoreServiceClientImpl);
return connection.createProxy(CoreServicePath, client);
}).inSingletonScope();
// Core service client to receive and delegate notifications when the index or the library index has been updated.
bind(CoreServiceClientImpl).toSelf().inSingletonScope();
bind(CoreServiceClient).toDynamicValue(context => {
const client = context.container.get(CoreServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, CoreServicePath, client);
return client;
}).inSingletonScope();
// Tool output service client
bind(ToolOutputServiceClientImpl).toSelf().inSingletonScope();
bind(ToolOutputServiceClient).toDynamicValue(context => {
const client = context.container.get(ToolOutputServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ToolOutputService.SERVICE_PATH, client);
return client;
}).inSingletonScope();
bind(CoreService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)).inSingletonScope();
// Serial monitor
bind(MonitorModel).toSelf().inSingletonScope();
@@ -303,6 +286,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
});
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
rebind(TheiaOutputChannelRegistryMainImpl).toService(OutputChannelRegistryMainImpl);
bind(MonacoTextModelService).toSelf().inSingletonScope();
rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
@@ -322,33 +311,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ProblemManager).toSelf().inSingletonScope();
rebind(TheiaProblemManager).toService(ProblemManager);
// About dialog to show the CLI version
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
// Arduino daemon client. Receives notifications from the backend if the CLI daemon process has been restarted.
bind(ArduinoDaemon).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(ArduinoDaemonClientImpl);
return connection.createProxy(ArduinoDaemonPath, client);
}).inSingletonScope();
bind(ArduinoDaemonClientImpl).toSelf().inSingletonScope();
bind(ArduinoDaemonClient).toDynamicValue(context => {
const client = context.container.get(ArduinoDaemonClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath, client);
return client;
}).inSingletonScope();
// No dropdown for the _Output_ view.
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
bind(ArduinoDaemon).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath)).inSingletonScope();
// File-system extension
bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope();
// Examples service@
bind(ExamplesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExamplesServicePath)).inSingletonScope();
// Executable URIs known by the backend
bind(ExecutableService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExecutableServicePath)).inSingletonScope();
Contribution.configure(bind, NewSketch);
Contribution.configure(bind, OpenSketch);
Contribution.configure(bind, CloseSketch);
Contribution.configure(bind, Close);
Contribution.configure(bind, SaveSketch);
Contribution.configure(bind, SaveAsSketch);
Contribution.configure(bind, VerifySketch);
@@ -358,4 +342,62 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, QuitApp);
Contribution.configure(bind, SketchControl);
Contribution.configure(bind, Settings);
Contribution.configure(bind, BurnBootloader);
Contribution.configure(bind, BuiltInExamples);
Contribution.configure(bind, LibraryExamples);
Contribution.configure(bind, IncludeLibrary);
Contribution.configure(bind, About);
Contribution.configure(bind, Debug);
Contribution.configure(bind, Sketchbook);
Contribution.configure(bind, BoardSelection);
Contribution.configure(bind, OpenRecentSketch);
Contribution.configure(bind, Help);
Contribution.configure(bind, AddFile);
Contribution.configure(bind, ArchiveSketch);
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
return outputService;
});
bind(OutputService).toService(OutputServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);
bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope();
// Enable the dirty indicator on uncloseable widgets.
rebind(TabBarRendererFactory).toFactory(context => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
const iconThemeService = context.container.get<IconThemeService>(IconThemeService);
return new TabBarRenderer(contextMenuRenderer, decoratorService, iconThemeService);
});
// 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.
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
// Silent the badge decoration in the Explorer view.
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution);
// Preferences
bindArduinoPreferences(bind);
// Settings wrapper for the preferences and the CLI config.
bind(SettingsService).toSelf().inSingletonScope();
// Settings dialog and widget
bind(SettingsWidget).toSelf().inSingletonScope();
bind(SettingsDialog).toSelf().inSingletonScope();
bind(SettingsDialogProps).toConstantValue({
title: 'Preferences'
});
});

View File

@@ -0,0 +1,73 @@
import { interfaces } from 'inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceContribution,
PreferenceSchema
} from '@theia/core/lib/browser/preferences';
export const ArduinoConfigSchema: PreferenceSchema = {
'type': 'object',
'properties': {
'arduino.language.log': {
'type': 'boolean',
'description': "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
'default': false
},
'arduino.compile.verbose': {
'type': 'boolean',
'description': 'True for verbose compile output. False by default',
'default': false
},
'arduino.upload.verbose': {
'type': 'boolean',
'description': 'True for verbose upload output. False by default.',
'default': false
},
'arduino.upload.verify': {
'type': 'boolean',
'default': false
},
'arduino.window.autoScale': {
'type': 'boolean',
'description': 'True if the user interface automatically scales with the font size.',
'default': true
},
'arduino.window.zoomLevel': {
'type': 'number',
'description': 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
'default': 0
},
'arduino.ide.autoUpdate': {
'type': 'boolean',
'description': 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
'default': true
}
}
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences(preferences: PreferenceService): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema);
}
export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
return createArduinoPreferences(preferences);
});
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema });
}

View File

@@ -1,8 +1,8 @@
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../widgets/progress-dialog';
import { BoardsConfig } from './boards-config';
@@ -20,8 +20,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
@@ -36,7 +36,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
if (selectedBoard) {
this.boardsService.search({}).then(packages => {
const candidates = packages
.filter(pkg => pkg.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length

View File

@@ -4,9 +4,8 @@ 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 { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CoreServiceClientImpl } from '../core-service-client-impl';
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
import { BoardsServiceProvider } from './boards-service-provider';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@@ -14,15 +13,13 @@ export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(CoreServiceClientImpl)
protected readonly coreServiceClient: CoreServiceClientImpl;
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
@@ -31,6 +28,14 @@ export class BoardsConfigDialogWidget extends ReactWidget {
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) => {
@@ -44,12 +49,11 @@ export class BoardsConfigDialogWidget extends ReactWidget {
protected render(): React.ReactNode {
return <div className='selectBoardContainer'>
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
coreServiceClient={this.coreServiceClient}
daemonClient={this.daemonClient}
boardsServiceProvider={this.boardsServiceClient}
notificationCenter={this.notificationCenter}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event} />
</div>;
}

View File

@@ -1,10 +1,10 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
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 { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {
@@ -19,8 +19,8 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected config: BoardsConfig.Config = {};
@@ -42,6 +42,16 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
}));
}
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
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');

View File

@@ -1,9 +1,11 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CoreServiceClientImpl } from '../core-service-client-impl';
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
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, AttachedBoardsChangeEvent, BoardWithPackage } from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
export namespace BoardsConfig {
@@ -13,16 +15,15 @@ export namespace BoardsConfig {
}
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly coreServiceClient: CoreServiceClientImpl;
readonly daemonClient: ArduinoDaemonClientImpl;
readonly boardsServiceProvider: BoardsServiceProvider;
readonly notificationCenter: NotificationCenter;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
}
export interface State extends Config {
searchResults: Array<Board & { packageName: string }>;
searchResults: Array<BoardWithPackage>;
knownPorts: Port[];
showAllPorts: boolean;
query: string;
@@ -70,7 +71,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceClient;
const { boardsConfig } = props.boardsServiceProvider;
this.state = {
searchResults: [],
knownPorts: [],
@@ -82,18 +83,18 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(ports => this.updatePorts(ports));
const { boardsServiceClient, coreServiceClient, daemonClient } = this.props;
this.updatePorts(this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty));
this.toDispose.pushAll([
boardsServiceClient.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
boardsServiceClient.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.props.notificationCenter.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
this.props.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
}),
boardsServiceClient.onBoardsPackageInstalled(() => this.updateBoards(this.state.query)),
boardsServiceClient.onBoardsPackageUninstalled(() => this.updateBoards(this.state.query)),
coreServiceClient.onIndexUpdated(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStarted(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStopped(() => this.setState({ searchResults: [] }))
this.props.notificationCenter.onPlatformInstalled(() => this.updateBoards(this.state.query)),
this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)),
this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)),
this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)),
this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })),
this.props.onFilteredTextDidChangeEvent(query => this.setState({ query }, () => this.updateBoards(this.state.query)))
]);
}
@@ -107,10 +108,9 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
}
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
const query = (typeof eventOrQuery === 'string'
const query = typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase()
).trim();
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
}
@@ -126,15 +126,15 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
});
}
protected queryBoards = (options: { query?: string } = {}): Promise<Array<Board & { packageName: string }>> => {
return this.props.boardsServiceClient.searchBoards(options);
protected queryBoards = (options: { query?: string } = {}): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
}
protected get availablePorts(): Promise<Port[]> {
return this.props.boardsService.getAvailablePorts();
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty);
}
protected queryPorts = async (availablePorts: Promise<Port[]> = this.availablePorts) => {
protected queryPorts = async (availablePorts: MaybePromise<Port[]> = this.availablePorts) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
}
@@ -147,7 +147,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
this.setState({ selectedPort }, () => this.fireConfigChanged());
}
protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => {
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
}
@@ -177,7 +177,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults } = this.state;
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>();
@@ -191,11 +191,18 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
return <React.Fragment>
<div className='search'>
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<input
type='search'
value={query}
className='theia-input'
placeholder='SEARCH BOARD'
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{Array.from(distinctBoards.values()).map(board => <Item<Board & { packageName: string }>
{Array.from(distinctBoards.values()).map(board => <Item<BoardWithPackage>
key={`${board.name}-${board.packageName}`}
item={board}
label={board.name}

View File

@@ -1,14 +1,14 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
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 { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@@ -25,8 +25,8 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
@@ -63,7 +63,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
Disposable.create(() => unregisterSubmenu(menuPath, this.menuRegistry)),
...Array.from(commands.keys()).map((commandId, i) => {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
@@ -76,7 +76,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(Disposable.create(() => this.unregisterSubmenu(programmersMenuPath)));
this.toDisposeOnBoardChange.push(Disposable.create(() => unregisterSubmenu(programmersMenuPath, this.menuRegistry)));
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
@@ -98,20 +98,4 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
});
}
protected unregisterSubmenu(menuPath: string[]): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = this.menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
}

View File

@@ -5,8 +5,8 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsDataStore implements FrontendApplicationContribution {
@@ -18,8 +18,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
@@ -27,13 +27,13 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => {
const { installedVersion: version } = pkg;
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
const { installedVersion: version } = item;
if (!version) {
return;
}
let shouldFireChanged = false;
for (const fqbn of pkg.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
for (const fqbn of item.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
if (!data || !data.length) {
@@ -58,27 +58,33 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
async appendConfigToFqbn(
fqbn: string,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string> {
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(
fqbn: string,
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const version = await boardsPackageVersion;
if (!version) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
if (data) {
if (data.programmers !== undefined) { // to be backward compatible. We did not save the `programmers` into the `localStorage`.
return data;
}
if (BoardsDataStore.Data.is(data)) {
return data;
}
const boardDetails = await this.getBoardDetailsSafe(fqbn);
@@ -151,7 +157,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
@@ -172,7 +178,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(fqbn: string): Promise<Installable.Version | undefined> {
protected async getBoardsPackageVersion(fqbn: string | undefined): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
@@ -196,5 +202,10 @@ export namespace BoardsDataStore {
configOptions: [],
programmers: []
};
export function is(arg: any): arg is Data {
return !!arg
&& 'configOptions' in arg && Array.isArray(arg['configOptions'])
&& 'programmers' in arg && Array.isArray(arg['programmers'])
}
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable, postConstruct } from 'inversify';
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@@ -24,4 +24,13 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
});
}
@postConstruct()
protected init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() => this.refresh(undefined)),
this.notificationCenter.onPlatformUninstalled(() => this.refresh(undefined)),
]);
}
}

View File

@@ -1,36 +1,51 @@
import { injectable, inject, optional } from 'inversify';
import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent, BoardsService } from '../../common/protocol';
import {
Port,
Board,
BoardsService,
BoardsPackage,
AttachedBoardsChangeEvent,
BoardWithPackage
} from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
import { compareAnything } from '../theia/monaco/comparers';
import { NotificationCenter } from '../notification-center';
import { CommandService } from '@theia/core';
import { ArduinoCommands } from '../arduino-commands';
interface BoardMatch {
readonly board: Board & Readonly<{ packageName: string }>;
readonly board: BoardWithPackage;
readonly matches: monaco.filters.IMatch[] | undefined;
}
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
export class BoardsServiceProvider implements FrontendApplicationContribution {
@inject(ILogger)
protected logger: ILogger;
@optional()
@inject(MessageService)
protected messageService: MessageService;
@inject(StorageService)
protected storageService: StorageService;
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
@inject(BoardsService)
protected boardsService: BoardsService;
@inject(CommandService)
protected commandService: CommandService;
@inject(NotificationCenter)
protected notificationCenter: NotificationCenter;
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
@@ -42,18 +57,12 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
protected _availablePorts: Port[] = [];
protected _availableBoards: AvailableBoard[] = [];
protected boardsService: BoardsService;
/**
* Event when the state of the attached/detached boards has changed. For instance, the user have detached a physical board.
*/
readonly onAttachedBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardsPackageInstalled = this.onBoardsPackageInstalledEmitter.event;
readonly onBoardsPackageUninstalled = this.onBoardsPackageUninstalledEmitter.event;
/**
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
* This even also fires, when the boards package was not available for the currently selected board,
@@ -64,33 +73,85 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
async onStart(): Promise<void> {
return this.loadState();
}
onStart(): void {
this.notificationCenter.onAttachedBoardsChanged(this.notifyAttachedBoardsChanged.bind(this));
this.notificationCenter.onPlatformInstalled(this.notifyPlatformInstalled.bind(this));
this.notificationCenter.onPlatformUninstalled(this.notifyPlatformUninstalled.bind(this));
/**
* When the FE connects to the BE, the BE stets the known boards and ports.\
* This is a DI workaround for not being able to inject the service into the client.
*/
async init(boardsService: BoardsService): Promise<void> {
this.boardsService = boardsService;
const [attachedBoards, availablePorts] = await Promise.all([
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]);
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
this.boardsService.getAvailablePorts(),
this.loadState()
]).then(([attachedBoards, availablePorts]) => {
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
});
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
protected notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
if (!AttachedBoardsChangeEvent.isEmpty(event)) {
this.logger.info('Attached boards and available ports changed:');
this.logger.info(AttachedBoardsChangeEvent.toString(event));
this.logger.info(`------------------------------------------`);
}
this._attachedBoards = event.newState.boards;
this.onAttachedBoardsChangedEmitter.fire(event);
this._availablePorts = event.newState.ports;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.logger.info('Boards package installed: ', JSON.stringify(event));
const { selectedBoard } = this.boardsConfig;
const { installedVersion, id } = event.item;
if (selectedBoard) {
const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: installedBoard
};
return;
}
// The board name can change after install.
// This logic handles it "gracefully" by unselecting the board, so that we can avoid no FQBN is set error.
// https://github.com/arduino/arduino-cli/issues/620
// https://github.com/arduino/arduino-pro-ide/issues/374
if (BoardWithPackage.is(selectedBoard) && selectedBoard.packageId === event.item.id && !installedBoard) {
this.messageService.warn(`Could not find previously selected board '${selectedBoard.name}' in installed platform '${event.item.name}'. Please manually reselect the board you want to use. Do you want to reselect it now?`, 'Reselect later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandService.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id, selectedBoard.name);
}
});
this.boardsConfig = {}
return;
}
// Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954
// E.g: install `adafruit:avr`, then select `adafruit:avr:adafruit32u4` board, and finally install the required `arduino:avr`
this.boardsConfig = this.boardsConfig;
}
}
protected notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
const { selectedBoard } = this.boardsConfig;
if (selectedBoard && selectedBoard.fqbn) {
const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
const selectedBoardWithoutFqbn = {
name: selectedBoard.name
// No FQBN
};
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: selectedBoardWithoutFqbn
};
}
}
}
protected async tryReconnect(): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
@@ -119,43 +180,6 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
return false;
}
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardsPackageInstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
const { installedVersion, id } = event.pkg;
if (selectedBoard) {
const installedBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: installedBoard
};
}
}
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardsPackageUninstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
if (selectedBoard && selectedBoard.fqbn) {
const uninstalledBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
this.logger.info(`Board package ${event.pkg.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
const selectedBoardWithoutFqbn = {
name: selectedBoard.name
// No FQBN
};
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: selectedBoardWithoutFqbn
};
}
}
}
set boardsConfig(config: BoardsConfig.Config) {
this.doSetBoardsConfig(config);
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
@@ -164,20 +188,21 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.latestBoardsConfig = this._boardsConfig;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
}
async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise<Array<Board & { packageName: string }>> {
async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise<Array<BoardWithPackage>> {
const boards = await this.boardsService.allBoards({});
const coresFilter = !!cores && cores.length
? ((toFilter: { packageName: string }) => cores.some(core => core === toFilter.packageName))
? ((toFilter: BoardWithPackage) => cores.some(core => core === toFilter.packageName || core === toFilter.packageId))
: () => true;
if (!query) {
return boards.filter(coresFilter).sort(Board.compare);
}
const toMatch = ((toFilter: Board & { packageName: string }) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) })));
const toMatch = ((toFilter: BoardWithPackage) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) })));
const compareEntries = (left: BoardMatch, right: BoardMatch, lookFor: string) => {
const leftMatches = left.matches || [];
const rightMatches = right.matches || [];
@@ -219,7 +244,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
if (!config.selectedBoard) {
if (!options.silent && this.messageService) {
if (!options.silent) {
this.messageService.warn('No boards selected.', { timeout: 3000 });
}
return false;
@@ -241,14 +266,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
const { name } = config.selectedBoard;
if (!config.selectedPort) {
if (!options.silent && this.messageService) {
if (!options.silent) {
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
}
return false;
}
if (!config.selectedBoard.fqbn) {
if (!options.silent && this.messageService) {
if (!options.silent) {
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
}
return false;
@@ -261,6 +286,29 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
return this._availableBoards;
}
async waitUntilAvailable(what: Board & { port: Port }, timeout?: number): Promise<void> {
const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) =>
haystack.find(board => Board.equals(needle, board) && Port.equals(needle.port, board.port));
const timeoutTask = !!timeout && timeout > 0
? new Promise<void>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout} ms.`)), timeout))
: new Promise<void>(() => { /* never */ });
const waitUntilTask = new Promise<void>(resolve => {
let candidate = find(what, this.availableBoards);
if (candidate) {
resolve();
return;
}
const disposable = this.onAvailableBoardsChanged(availableBoards => {
candidate = find(what, availableBoards);
if (candidate) {
disposable.dispose();
resolve();
}
});
});
return await Promise.race([waitUntilTask, timeoutTask]);
}
protected async reconcileAvailableBoards(): Promise<void> {
const attachedBoards = this._attachedBoards;
const availablePorts = this._availablePorts;
@@ -338,7 +386,10 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
await this.storageService.setData(key, selectedBoard);
}
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
await Promise.all([
this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
this.storageService.setData('latest-boards-config', this.latestBoardsConfig)
]);
}
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
@@ -347,15 +398,21 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
const storedLatestValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedLatestValidBoardsConfig) {
this.latestValidBoardsConfig = storedLatestValidBoardsConfig;
if (this.canUploadTo(this.latestValidBoardsConfig)) {
this.boardsConfig = this.latestValidBoardsConfig;
}
} else {
// If we could not restore the latest valid config, try to restore something, the board at least.
const storedLatestBoardsConfig = await this.storageService.getData<BoardsConfig.Config | undefined>('latest-boards-config');
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
this.boardsConfig = this.latestBoardsConfig;
}
}
}
}
/**

View File

@@ -5,7 +5,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl, AvailableBoard } from './boards-service-client-impl';
import { BoardsServiceProvider, AvailableBoard } from './boards-service-provider';
export interface BoardsDropDownListCoords {
readonly top: number;
@@ -181,7 +181,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
export namespace BoardsToolBarItem {
export interface Props {
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly boardsServiceClient: BoardsServiceProvider;
readonly commands: CommandRegistry;
}

View File

@@ -1,15 +1,11 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { BoardsPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
constructor() {
super({
widgetId: BoardsListWidget.WIDGET_ID,
@@ -18,7 +14,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
area: 'left',
rank: 600
},
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+B'
});
}
@@ -27,14 +23,4 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...',
order: '4'
});
}
}
}

View File

@@ -16,9 +16,9 @@ import {
} from '@theia/core/lib/browser/quick-open';
import { naturalCompare } from '../../../common/utils';
import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol';
import { CoreServiceClientImpl } from '../../core-service-client-impl';
import { BoardsDataStore } from '../boards-data-store';
import { BoardsServiceClientImpl, AvailableBoard } from '../boards-service-client-impl';
import { BoardsServiceProvider, AvailableBoard } from '../boards-service-provider';
import { NotificationCenter } from '../../notification-center';
@injectable()
export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command {
@@ -38,14 +38,14 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(CoreServiceClientImpl)
protected coreServiceClient: CoreServiceClientImpl;
@inject(NotificationCenter)
protected notificationCenter: NotificationCenter;
protected isOpen: boolean = false;
protected currentQuery: string = '';
@@ -59,7 +59,7 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
// `init` name is used by the `QuickOpenHandler`.
@postConstruct()
protected postConstruct(): void {
this.coreServiceClient.onIndexUpdated(() => this.update(this.availableBoards));
this.notificationCenter.onIndexUpdated(() => this.update(this.availableBoards));
this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards));
this.update(this.boardsServiceClient.availableBoards);
}

View File

@@ -1,52 +0,0 @@
import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ConfigServiceClient, Config } from '../common/protocol';
import { Settings } from './contributions/settings';
@injectable()
export class ConfigServiceClientImpl implements ConfigServiceClient {
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
protected readonly onConfigChangedEmitter = new Emitter<Config>();
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
notifyConfigChanged(config: Config): void {
this.invalidConfigPopup = undefined;
this.info(`The CLI configuration has been successfully reloaded.`);
this.onConfigChangedEmitter.fire(config);
}
notifyInvalidConfig(): void {
if (!this.invalidConfigPopup) {
this.invalidConfigPopup = this.messageService.error(`Your CLI configuration is invalid. Do you want to correct it now?`, 'No', 'Yes')
.then(answer => {
if (answer === 'Yes') {
this.commandService.executeCommand(Settings.Commands.OPEN_CLI_CONFIG.id)
}
this.invalidConfigPopup = undefined;
})
}
}
get onConfigChanged(): Event<Config> {
return this.onConfigChangedEmitter.event;
}
protected info(message: string): void {
this.messageService.info(message, { timeout: 3000 });
this.logger.info(message);
}
}

View File

@@ -0,0 +1,105 @@
import { inject, injectable } from 'inversify';
import * as moment from 'moment';
import { remote } from 'electron';
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 { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol';
@injectable()
export class About extends Contribution {
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
@inject(ConfigService)
protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id,
label: `About ${this.applicationName}`,
order: '0'
});
}
async showAbout(): Promise<void> {
const { version, commit, status: cliStatus } = await this.configService.getVersion();
const buildDate = this.buildDate;
const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${buildDate && showAll ? ` (${this.ago(buildDate)})` : ''}
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
`;
const ok = 'OK';
const 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)
});
if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false).trim());
}
}
protected get applicationName(): string {
return FrontendApplicationConfigProvider.get().applicationName;
}
protected get buildDate(): string | undefined {
return FrontendApplicationConfigProvider.get().buildDate;
}
protected ago(isoTime: string): string {
const now = moment(Date.now());
const other = moment(isoTime);
let result = now.diff(other, 'minute');
if (result < 60) {
return result === 1 ? `${result} minute ago` : `${result} minute ago`;
}
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1 ? `${result} month ago` : `${result} months ago`;
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
}
}
export namespace About {
export namespace Commands {
export const ABOUT_APP: Command = {
id: 'arduino-about'
};
}
}

View File

@@ -0,0 +1,68 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, URI } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id,
label: 'Add File...',
order: '2'
});
}
protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`
});
if (response === 0) { // Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', { timeout: 2000 });
}
}
export namespace AddFile {
export namespace Commands {
export const ADD_FILE: Command = {
id: 'arduino-add-file'
};
}
}

View File

@@ -0,0 +1,55 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
@injectable()
export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: 'Archive Sketch',
order: '1'
});
}
protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration()
]);
if (!sketch) {
return;
}
const archiveBasename = `${sketch.name}-${dateFormat(new Date(), 'yymmdd')}a.zip`;
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri).resolve(archiveBasename));
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, { timeout: 2000 });
}
}
export namespace ArchiveSketch {
export namespace Commands {
export const ARCHIVE_SKETCH: Command = {
id: 'arduino-archive-sketch'
};
}
}

View File

@@ -0,0 +1,222 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
@injectable()
export class BoardSelection extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } = this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) {
this.messageService.info('Please select a board to obtain board info.');
return;
}
if (!selectedBoard.fqbn) {
this.messageService.info(`The platform for the selected '${selectedBoard.name}' board is not installed.`);
return;
}
if (!selectedPort) {
this.messageService.info('Please select a port to obtain board info.');
return;
}
const boardDetails = await this.boardsService.getBoardDetails({ fqbn: selectedBoard.fqbn });
if (boardDetails) {
const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name}
VID: ${VID}
PID: ${PID}`;
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: 'Board Info',
title: 'Board Info',
type: 'info',
detail,
buttons: ['OK']
});
}
}
});
}
onStart(): void {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(this.updateMenus.bind(this));
this.boardsServiceProvider.onAvailableBoardsChanged(this.updateMenus.bind(this));
}
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);
}
protected rebuildMenus(installedBoards: InstalledBoardWithPackage[], availablePorts: AvailablePorts, config: BoardsConfig.Config): void {
this.toDisposeBeforeMenuRebuild.dispose();
// Boards submenu
const boardsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards'];
const boardsSubmenuLabel = config.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(boardsSubmenuPath, `Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`, { order: '100' });
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)));
// Ports submenu
const portsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '2_ports'];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(portsSubmenuPath, `Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`, { order: '101' });
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)));
const getBoardInfo = { commandId: BoardSelection.Commands.GET_BOARD_INFO.id, label: 'Get Board Info', order: '103' };
this.menuModelRegistry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, getBoardInfo);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuAction(getBoardInfo)));
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...'
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
// 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, packageName);
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
}
}
},
isToggled: () => fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
};
// Board menu
const menuAction = { commandId: id, label: name };
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
const registerPorts = (ports: AvailablePorts) => {
const addresses = Object.keys(ports);
if (!addresses.length) {
return;
}
// Register placeholder for protocol
const [port] = ports[addresses[0]];
const protocol = port.protocol;
const menuPath = [...portsSubmenuPath, protocol];
const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
for (const address of addresses) {
if (!!ports[address]) {
const [port, boards] = ports[address];
if (!boards.length) {
boards.push({
name: ''
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
const command = { id };
const handler = {
execute: () => {
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port
}
}
},
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
};
const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = {
commandId: id,
label,
order: `1${label}` // `1` comes after the placeholder which has order `0`
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
}
}
}
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
this.mainMenuManager.update();
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.allBoards({});
return allBoards.filter(InstalledBoardWithPackage.is);
}
}
export namespace BoardSelection {
export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
}
}

View File

@@ -0,0 +1,82 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: 'Burn Bootloader',
order: 'z99'
});
}
async burnBootloader(): Promise<void> {
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort?.address;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose')
]);
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
programmer,
port,
verify,
verbose
});
this.messageService.info('Done burning bootloader.', { timeout: 1000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
}
}
export namespace BurnBootloader {
export namespace Commands {
export const BURN_BOOTLOADER: Command = {
id: 'arduino-burn-bootloader'
};
}
}

View File

@@ -1,20 +1,50 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { EditorManager } from '@theia/editor/lib/browser';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
/**
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
*/
@injectable()
export class CloseSketch extends SketchContribution {
export class Close extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected shell: ApplicationShell;
onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => {
// Close current editor if closeable.
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) {
currentEditor.close();
return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(this.shell.mainPanel.widgets()).find(widget => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
@@ -48,7 +78,7 @@ export class CloseSketch extends SketchContribution {
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: CloseSketch.Commands.CLOSE_SKETCH.id,
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5'
});
@@ -56,7 +86,7 @@ export class CloseSketch extends SketchContribution {
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: CloseSketch.Commands.CLOSE_SKETCH.id,
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W'
});
}
@@ -80,10 +110,10 @@ export class CloseSketch extends SketchContribution {
}
export namespace CloseSketch {
export namespace Close {
export namespace Commands {
export const CLOSE_SKETCH: Command = {
id: 'arduino-close-sketch'
export const CLOSE: Command = {
id: 'arduino-close'
};
}
}

View File

@@ -1,23 +1,29 @@
import { inject, injectable, interfaces } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileSystem } from '@theia/filesystem/lib/common';
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 { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu';
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';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@injectable()
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution {
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
@inject(ILogger)
protected readonly logger: ILogger;
@@ -37,6 +43,12 @@ export abstract class Contribution implements CommandContribution, MenuContribut
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SettingsService)
protected readonly settingsService: SettingsService;
onStart(app: FrontendApplication): MaybePromise<void> {
}
registerCommands(registry: CommandRegistry): void {
}
@@ -54,8 +66,8 @@ export abstract class Contribution implements CommandContribution, MenuContribut
@injectable()
export abstract class SketchContribution extends Contribution {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@@ -72,14 +84,35 @@ export abstract class SketchContribution extends Contribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
override[uri.toString()] = editor.editor.document.getText();
}
}
}
return override;
}
}
export namespace Contribution {
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: interfaces.ServiceIdentifier<T>): void {
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: typeof Contribution): void {
bind(serviceIdentifier).toSelf().inSingletonScope();
bind(CommandContribution).toService(serviceIdentifier);
bind(MenuContribution).toService(serviceIdentifier);
bind(KeybindingContribution).toService(serviceIdentifier);
bind(TabBarToolbarContribution).toService(serviceIdentifier);
bind(FrontendApplicationContribution).toService(serviceIdentifier);
}
}

View File

@@ -0,0 +1,134 @@
import { inject, injectable } from '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 { Board, BoardsService, ExecutableService } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { URI, Command, CommandRegistry, SketchContribution, TabBarToolbarRegistry } from './contribution';
@injectable()
export class Debug extends SketchContribution {
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(ExecutableService)
protected readonly executableService: ExecutableService;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
protected onDisabledMessageDidChange = this.disabledMessageDidChangeEmitter.event;
protected get disabledMessage(): string | undefined {
return this._disabledMessages;
}
protected set disabledMessage(message: string | undefined) {
this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
}
protected readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`,
priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>
};
onStart(): void {
this.onDisabledMessageDidChange(() => this.debugToolbarItem.tooltip = `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`);
const refreshState = async (board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
}
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => refreshState(selectedBoard));
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
execute: () => this.startDebug(),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem(this.debugToolbarItem);
}
protected async startDebug(board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard): Promise<void> {
if (!board) {
return;
}
const { name, fqbn } = board;
if (!fqbn) {
return;
}
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list()
]);
if (!sketch) {
return;
}
const [cliPath, sketchPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri))
])
const config = {
cliPath,
board: {
fqbn,
name
},
sketchPath
};
return this.commandService.executeCommand('arduino.debug.start', config);
}
}
export namespace Debug {
export namespace Commands {
export const START_DEBUGGING: Command = {
id: 'arduino-start-debug',
label: 'Start Debugging',
category: 'Arduino'
}
}
}

View File

@@ -3,7 +3,6 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { EDITOR_FONT_DEFAULTS } from '@theia/editor/lib/browser/editor-preferences';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -31,10 +30,28 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { execute: () => this.run('editor.action.nextMatchFindAction') });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) + 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) - 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {

View File

@@ -0,0 +1,169 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board } from '../../common/protocol';
@injectable()
export abstract class Examples extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly toDispose = new DisposableCollection();
@postConstruct()
init(): void {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
}
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
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(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', { order: '4' });
}
registerRecursively(
exampleContainerOrPlaceholder: ExampleContainer | string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection()): void {
if (typeof exampleContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
} else {
const { label, sketches, children } = exampleContainerOrPlaceholder;
const submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label);
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) {
const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
const command = { id: commandId };
const handler = {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
}
};
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
}
}
}
@injectable()
export class BuiltInExamples extends Examples {
onStart(): void {
this.register(); // no `await`
}
protected async register() {
let exampleContainers: ExampleContainer[] | undefined;
try {
exampleContainers = await this.examplesService.builtIns();
} catch (e) {
console.error('Could not initialize built-in examples.', e);
this.messageService.error('Could not initialize built-in examples.');
return;
}
this.toDispose.dispose();
for (const container of ['Built-in examples', ...exampleContainers]) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
}
this.menuManager.update();
// TODO: remove
console.log(typeof this.menuRegistry);
}
}
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void {
this.register(); // no `await`
this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register());
}
protected handleBoardChanged(board: Board | undefined): void {
this.register(board);
}
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
return this.queue.add(async () => {
this.toDispose.dispose();
if (!board || !board.fqbn) {
return;
}
const { fqbn, name } = board;
const { user, current, any } = await this.examplesService.installed({ fqbn });
if (user.length) {
(user as any).unshift('Examples from Custom Libraries');
}
if (current.length) {
(current as any).unshift(`Examples for ${name}`);
}
if (any.length) {
(any as any).unshift('Examples for any board');
}
for (const container of user) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
}
for (const container of current) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, this.toDispose);
}
for (const container of any) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, this.toDispose);
}
this.menuManager.update();
});
}
}

View File

@@ -0,0 +1,137 @@
import { inject, injectable } from 'inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandHandler } from '@theia/core/lib/common/command';
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Contribution, Command, MenuModelRegistry, CommandRegistry, KeybindingRegistry } from './contribution';
@injectable()
export class Help extends Contribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void {
const open = (url: string) => this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) => <CommandHandler>{
execute: () => open(url)
};
registry.registerCommand(Help.Commands.GETTING_STARTED, createOpenHandler('https://www.arduino.cc/en/Guide'));
registry.registerCommand(Help.Commands.ENVIRONMENT, createOpenHandler('https://www.arduino.cc/en/Guide/Environment'));
registry.registerCommand(Help.Commands.TROUBLESHOOTING, createOpenHandler('https://support.arduino.cc/hc/en-us'));
registry.registerCommand(Help.Commands.REFERENCE, createOpenHandler('https://www.arduino.cc/reference/en/'));
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
execute: async () => {
let searchFor: string | undefined = undefined;
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
const codeEditor = currentEditor.editor.getControl();
const selection = codeEditor.getSelection();
const model = codeEditor.getModel();
if (model && selection && !monaco.Range.isEmpty(selection)) {
searchFor = model.getValueInRange(selection);
}
}
if (!searchFor) {
searchFor = await this.quickInputService.open({
prompt: 'Search on Arduino.cc',
placeHolder: 'Type a keyword'
});
}
if (searchFor) {
return open(`https://www.arduino.cc/search?q=${encodeURIComponent(searchFor)}&tab=reference`);
}
}
});
registry.registerCommand(Help.Commands.FAQ, createOpenHandler('https://support.arduino.cc/hc/en-us'));
registry.registerCommand(Help.Commands.VISIT_ARDUINO, createOpenHandler('https://www.arduino.cc/'));
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.GETTING_STARTED.id,
order: '0'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.ENVIRONMENT.id,
order: '1'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.TROUBLESHOOTING.id,
order: '2'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.REFERENCE.id,
order: '3'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FIND_IN_REFERENCE.id,
order: '4'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FAQ.id,
order: '5'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.VISIT_ARDUINO.id,
order: '6'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F'
});
}
}
export namespace Help {
export namespace Commands {
export const GETTING_STARTED: Command = {
id: 'arduino-getting-started',
label: 'Getting Started',
category: 'Arduino'
};
export const ENVIRONMENT: Command = {
id: 'arduino-environment',
label: 'Environment',
category: 'Arduino'
};
export const TROUBLESHOOTING: Command = {
id: 'arduino-troubleshooting',
label: 'Troubleshooting',
category: 'Arduino'
};
export const REFERENCE: Command = {
id: 'arduino-reference',
label: 'Reference',
category: 'Arduino'
};
export const FIND_IN_REFERENCE: Command = {
id: 'arduino-find-in-reference',
label: 'Find in Reference',
category: 'Arduino'
};
export const FAQ: Command = {
id: 'arduino-faq',
label: 'Frequently Asked Questions',
category: 'Arduino'
};
export const VISIT_ARDUINO: Command = {
id: 'arduino-visit-arduino',
label: 'Visit Arduino.cc',
category: 'Arduino'
};
}
}

View File

@@ -0,0 +1,175 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from '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, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { LibraryPackage, LibraryService } from '../../common/protocol';
import { MainMenuManager } from '../../common/main-menu-manager';
import { LibraryListWidget } from '../library/library-list-widget';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
@injectable()
export class IncludeLibrary extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
onStart(): void {
this.updateMenuActions();
this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions())
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions());
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async arg => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
}
});
}
protected 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;
// Do not show board specific examples, when no board is selected.
if (fqbn) {
libraries.push(...await this.libraryService.list({ fqbn }));
}
// `Include Library` submenu
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
this.menuRegistry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
// `Manage Libraries...` group.
this.menuRegistry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...'
});
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction({ commandId: `${LibraryListWidget.WIDGET_ID}:toggle` })));
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(this.registerLibrary(library, userMenuPath));
}
for (const library of rest) {
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
}
this.mainMenuManager.update();
});
}
protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(menuAction)),
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.
// Otherwise, we pick the editor of the main sketch file.
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
const editor = this.editorManager.currentEditor?.editor;
if (editor instanceof MonacoEditor) {
if (sketch.additionalFileUris.some(uri => uri === editor.uri.toString())) {
codeEditor = editor.getControl();
}
}
if (!codeEditor) {
const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
if (widget.editor instanceof MonacoEditor) {
codeEditor = widget.editor.getControl();
}
}
if (!codeEditor) {
return;
}
const textModel = codeEditor.getModel();
if (!textModel) {
return;
}
const cursorState = codeEditor.getSelections() || [];
const eol = textModel.getEOL();
const includes = library.includes.slice();
includes.push(''); // For the trailing new line.
const text = includes.map(include => include ? `#include <${include}>` : eol).join(eol);
textModel.pushStackElement(); // Start a fresh operation.
textModel.pushEditOperations(cursorState, [{
range: new monaco.Range(1, 1, 1, 1),
text,
forceMoveMarkers: true
}], () => cursorState);
textModel.pushStackElement(); // Make it undoable.
}
}
export namespace IncludeLibrary {
export namespace Commands {
export const INCLUDE_LIBRARY: Command = {
id: 'arduino-include-library'
};
}
}

View File

@@ -0,0 +1,62 @@
import { inject, injectable } from 'inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void {
const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches);
this.mainMenuManager.update();
};
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => refreshMenu(sketches));
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, 'Open Recent', { order: '2' });
}
protected register(sketches: Sketch[]): void {
let order = 0;
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` };
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, { commandId: command.id, label: sketch.name, order: String(order) });
this.toDisposeBeforeRegister.set(sketch.uri, new DisposableCollection(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
));
}
}
}

View File

@@ -1,5 +1,6 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
@@ -30,9 +31,9 @@ export class OpenSketchExternal extends SketchContribution {
protected async openExternal(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) {
const exists = this.fileSystem.exists(uri);
const exists = this.fileService.exists(new URI(uri));
if (exists) {
const fsPath = await this.fileSystem.getFsPath(uri);
const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) {
remote.shell.showItemInFolder(fsPath);
}

View File

@@ -6,6 +6,8 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples';
@injectable()
export class OpenSketch extends SketchContribution {
@@ -16,6 +18,12 @@ export class OpenSketch extends SketchContribution {
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(BuiltInExamples)
protected readonly builtInExamples: BuiltInExamples;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
@@ -53,6 +61,14 @@ export class OpenSketch extends SketchContribution {
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
try {
const containers = await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
@@ -99,14 +115,14 @@ export class OpenSketch extends SketchContribution {
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileSystem.getFsPath(config.sketchDirUri);
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri));
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino']
extensions: ['ino', 'pde']
}
]
});
@@ -122,7 +138,7 @@ export class OpenSketch extends SketchContribution {
if (sketch) {
return sketch;
}
if (sketchFileUri.endsWith('.ino')) {
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
@@ -133,7 +149,7 @@ export class OpenSketch extends SketchContribution {
});
if (response === 1) { // OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileSystem.exists(newSketchUri.toString());
const exists = await this.fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
@@ -142,8 +158,8 @@ export class OpenSketch extends SketchContribution {
});
return undefined;
}
await this.fileSystem.createFolder(newSketchUri.toString());
await this.fileSystem.move(sketchFileUri, newSketchUri.resolve(nameWithExt).toString());
await this.fileService.createFolder(newSketchUri);
await this.fileService.move(new URI(sketchFileUri), new URI(newSketchUri.resolve(nameWithExt).toString()));
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}

View File

@@ -45,11 +45,11 @@ export class SaveAsSketch extends SketchContribution {
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri);
const exists = await this.fileSystem.exists(sketchDirUri.resolve(sketch.name).toString());
const exists = await this.fileService.exists(sketchDirUri.resolve(sketch.name));
const defaultUri = exists
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileSystem.getFsPath(defaultUri.toString())!;
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
if (!filePath || canceled) {
return false;
@@ -60,8 +60,10 @@ export class SaveAsSketch extends SketchContribution {
}
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
if (workspaceUri && openAfterMove) {
if (wipeOriginal) {
await this.fileSystem.delete(sketch.uri);
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
} catch { /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ }
}
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
}

View File

@@ -1,27 +1,49 @@
import { injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { URI, Command, MenuModelRegistry, CommandRegistry, SketchContribution, open } from './contribution';
import { inject, injectable } from 'inversify';
import { Command, MenuModelRegistry, CommandRegistry, SketchContribution, KeybindingRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings';
@injectable()
export class Settings extends SketchContribution {
@inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog;
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN_CLI_CONFIG, {
execute: () => this.configService.getCliConfigFileUri().then(uri => open(this.openerService, new URI(uri)))
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
try {
this.settingsOpened = true;
settings = await this.settingsDialog.open();
} finally {
this.settingsOpened = false;
}
if (settings) {
await this.settingsService.update(settings);
await this.settingsService.save();
} else {
await this.settingsService.reset();
}
},
isEnabled: () => !this.settingsOpened
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: CommonCommands.OPEN_PREFERENCES.id,
commandId: Settings.Commands.OPEN.id,
label: 'Preferences...',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: Settings.Commands.OPEN_CLI_CONFIG.id,
label: 'Open CLI Configuration',
order: '1',
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
});
}
@@ -29,9 +51,9 @@ export class Settings extends SketchContribution {
export namespace Settings {
export namespace Commands {
export const OPEN_CLI_CONFIG: Command = {
id: 'arduino-open-cli-config',
label: 'Open CLI Configuration',
export const OPEN: Command = {
id: 'arduino-settings-open',
label: 'Open Preferences...',
category: 'Arduino'
}
}

View File

@@ -40,8 +40,8 @@ export class SketchControl extends SketchContribution {
return;
}
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
const { mainFileUri, rootFolderFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...rootFolderFileUris];
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
const command = { id: `arduino-focus-file--${uri.toString()}` };

View File

@@ -0,0 +1,69 @@
import { inject, injectable } from 'inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { OpenSketch } from './open-sketch';
@injectable()
export class Sketchbook extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposePerSketch = new Map<string, DisposableCollection>();
onStart(): void {
this.sketchService.getSketches().then(sketches => {
this.register(sketches);
this.mainMenuManager.update();
});
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
this.unregister(removed);
this.register(created);
this.mainMenuManager.update();
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
}
protected register(sketches: Sketch[]): void {
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-sketchbook-open--${uri}` };
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
));
}
}
protected unregister(sketches: Sketch[]): void {
for (const { uri } of sketches) {
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
}
}
}

View File

@@ -5,7 +5,7 @@ import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
@@ -20,8 +20,8 @@ export class UploadSketch extends SketchContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@@ -43,12 +43,12 @@ export class UploadSketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '0'
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '1'
order: '2'
});
}
@@ -73,71 +73,81 @@ export class UploadSketch extends SketchContribution {
}
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
let shouldAutoConnect = false;
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
if (this.monitorConnection.autoConnect) {
shouldAutoConnect = true;
}
this.monitorConnection.autoConnect = false;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const [fqbn, { selectedProgrammer }] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard.fqbn)
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
this.sourceOverride()
]);
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = uri;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort?.address;
if (usingProgrammer) {
const programmer = selectedProgrammer;
if (!programmer) {
throw new Error('Programmer is not selected. Please select a programmer.');
}
let port: undefined | string = undefined;
// If the port is set by the user, we pass it to the CLI as it might be required.
// If it is not set but the CLI requires it, we let the CLI to complain.
if (selectedPort) {
port = selectedPort.address;
}
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer,
port
port,
verbose,
verify,
sourceOverride
};
} else {
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
const port = selectedPort.address;
options = {
sketchUri,
fqbn,
optimizeForDebug,
port
port,
verbose,
verify,
sourceOverride
};
}
this.outputChannelManager.getChannel('Arduino: upload').clear();
await this.coreService.upload(options);
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 1000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
const { board, port } = monitorConfig;
try {
await this.boardsServiceClientImpl.waitUntilAvailable(Object.assign(board, { port }), 10_000);
if (shouldAutoConnect) {
// Enabling auto-connect will trigger a connect.
this.monitorConnection.autoConnect = true;
} else {
await this.monitorConnection.connect(monitorConfig);
}
} catch (waitError) {
this.messageService.error(`Could not reconnect to serial monitor. ${waitError.toString()}`);
}
}
}
}

View File

@@ -4,7 +4,7 @@ import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
@@ -16,8 +16,8 @@ export class VerifySketch extends SketchContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@@ -26,6 +26,9 @@ export class VerifySketch extends SketchContribution {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch()
});
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true)
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id)
@@ -36,7 +39,12 @@ export class VerifySketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
order: '2'
order: '0'
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
label: 'Export compiled Binary',
order: '3'
});
}
@@ -45,6 +53,10 @@ export class VerifySketch extends SketchContribution {
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R'
});
registry.registerKeybinding({
command: VerifySketch.Commands.EXPORT_BINARIES.id,
keybinding: 'CtrlCmd+Alt+S'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -56,25 +68,26 @@ export class VerifySketch extends SketchContribution {
});
}
async verifySketch(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
async verifySketch(exportBinaries?: boolean): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
this.outputChannelManager.getChannel('Arduino: compile').clear();
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.sourceOverride()
]);
const verbose = this.preferences.get('arduino.compile.verbose');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: uri,
sketchUri: sketch.uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride
});
this.messageService.info('Done compiling.', { timeout: 1000 });
} catch (e) {
@@ -89,6 +102,9 @@ export namespace VerifySketch {
export const VERIFY_SKETCH: Command = {
id: 'arduino-verify-sketch'
};
export const EXPORT_BINARIES: Command = {
id: 'arduino-export-binaries'
};
export const VERIFY_SKETCH_TOOLBAR: Command = {
id: 'arduino-verify-sketch--toolbar'
};

View File

@@ -1,36 +0,0 @@
import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { CoreServiceClient } from '../common/protocol';
@injectable()
export class CoreServiceClientImpl implements CoreServiceClient {
@inject(ILogger)
protected logger: ILogger;
@inject(MessageService)
protected messageService: MessageService;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onIndexUpdatedEmitter = new Emitter<void>();
notifyIndexUpdated(): void {
this.info('Index has been updated.');
this.onIndexUpdatedEmitter.fire();
}
get onIndexUpdated(): Event<void> {
return this.onIndexUpdatedEmitter.event;
}
protected info(message: string): void {
this.messageService.info(message, { timeout: 3000 });
this.logger.info(message);
}
}

View File

@@ -1,10 +1,6 @@
import { injectable, inject } from 'inversify';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsListWidget } from './boards/boards-list-widget';
import { LibraryListWidget } from './library/library-list-widget';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
@@ -16,41 +12,6 @@ export class EditorMode implements FrontendApplicationContribution {
onStart(app: FrontendApplication): void {
this.app = app;
if (this.proMode) {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(EditorMode.PRO_MODE_KEY);
}
}
get proMode(): boolean {
const value = window.localStorage.getItem(EditorMode.PRO_MODE_KEY);
return value === 'true';
}
async toggleProMode(): Promise<void> {
const oldState = this.proMode;
const inAdvancedMode = !oldState;
window.localStorage.setItem(EditorMode.PRO_MODE_KEY, String(inAdvancedMode));
if (!inAdvancedMode) {
const { shell } = this.app;
// Close all widgets that are neither editor nor `Output` / `Boards Manager` / `Library Manager`.
for (const area of ['left', 'right', 'bottom', 'main'] as Array<ApplicationShell.Area>) {
shell.closeTabs(area, title => !this.isInSimpleMode(title.owner));
}
}
// `storeLayout` has a sync API but the implementation is async, we store the layout manually before we reload the page.
// See: https://github.com/eclipse-theia/theia/issues/6579
// XXX: hack instead of injecting the `ArduinoShellLayoutRestorer` we have to retrieve it from the application to avoid DI cycle.
const layoutRestorer = (this.app as any).layoutRestorer as { storeLayoutAsync(app: FrontendApplication): Promise<void> };
await layoutRestorer.storeLayoutAsync(this.app);
window.location.reload(true);
}
protected isInSimpleMode(widget: Widget): boolean {
return widget instanceof EditorWidget
|| widget instanceof OutputWidget
|| widget instanceof BoardsListWidget
|| widget instanceof LibraryListWidget;
}
get compileForDebug(): boolean {
@@ -68,6 +29,5 @@ export class EditorMode implements FrontendApplicationContribution {
}
export namespace EditorMode {
export const PRO_MODE_KEY = 'arduino-advanced-mode';
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
}

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="24" height="24" viewBox="0, 0, 24, 24">
<g>
<path d="M16.2 8.1c0-4.5-3.6-8.1-8.1-8.1S0 3.6 0 8.1s3.6 8.1 8.1 8.1c1.9 0 3.7-.7 5.1-1.8l5.6 5.6 1.4-1.4-5.7-5.6c1.1-1.4 1.7-3.1 1.7-4.9zm-14.4 0c0-3.5 2.8-6.3 6.3-6.3s6.3 2.8 6.3 6.3-2.8 6.3-6.3 6.3c-3.5.1-6.3-2.8-6.3-6.3z" />
<rect x="7.1" y="7.1" width="2" height="2" />
<rect x="17.2" y="7.1" width="2" height="2" />
<rect x="20.3" y="7.1" width="2" height="2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@@ -1,40 +0,0 @@
import { injectable, inject, postConstruct } from 'inversify';
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
@injectable()
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
readonly id = 'ino';
readonly name = 'Arduino';
protected get documentSelector(): string[] {
return ['ino'];
}
protected get globPatterns() {
return ['**/*.ino'];
}
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected boardConfig?: BoardsConfig.Config;
@postConstruct()
protected init() {
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
}
selectBoard(config: BoardsConfig.Config): void {
this.boardConfig = config;
// Force a restart to send the new board config to the language server
this.restart();
}
protected getStartParameters(): BoardsConfig.Config | undefined {
return this.boardConfig;
}
}

View File

@@ -1,63 +0,0 @@
import { injectable } from 'inversify';
import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate';
@injectable()
export class ArduinoLanguageGrammarContribution implements LanguageGrammarDefinitionContribution {
static INO_LANGUAGE_ID = 'ino';
registerTextmateLanguage(registry: TextmateRegistry) {
monaco.languages.register({
id: ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID,
extensions: ['.ino'],
aliases: ['INO', 'Ino', 'ino'],
});
monaco.languages.setLanguageConfiguration(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, this.configuration);
const inoGrammar = require('../../../data/ino.tmLanguage.json');
registry.registerTextmateGrammarScope('source.ino', {
async getGrammarDefinition() {
return {
format: 'json',
content: inoGrammar
};
}
});
registry.mapLanguageIdToTextmateGrammar(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, 'source.ino');
}
private readonly configuration: monaco.languages.LanguageConfiguration = {
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
autoClosingPairs: [
{ open: '[', close: ']' },
{ open: '{', close: '}' },
{ open: '(', close: ')' },
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '/*', close: ' */', notIn: ['string'] }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: '\'', close: '\'' },
],
folding: {
markers: {
start: new RegExp('^\\s*#pragma\\s+region\\b'),
end: new RegExp('^\\s*#pragma\\s+endregion\\b')
}
}
};
}

View File

@@ -1,17 +1,17 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { injectable, postConstruct, inject } from 'inversify';
import { LibraryPackage, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
export class LibraryListWidget extends ListWidget<LibraryPackage> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<LibraryPackage>) {
super({
id: LibraryListWidget.WIDGET_ID,
@@ -19,9 +19,18 @@ export class LibraryListWidget extends ListWidget<Library> {
iconClass: 'library-tab-icon',
searchable: service,
installable: service,
itemLabel: (item: Library) => item.name,
itemLabel: (item: LibraryPackage) => item.name,
itemRenderer
});
}
@postConstruct()
protected init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
this.notificationCenter.onLibraryUninstalled(() => this.refresh(undefined)),
]);
}
}

View File

@@ -1,6 +1,6 @@
import { MAIN_MENU_BAR } from '@theia/core/lib/common/menu';
import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { isOSX } from '@theia/core';
import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode, MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
export namespace ArduinoMenus {
@@ -12,6 +12,19 @@ export namespace ArduinoMenus {
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
// -- File / Open Recent
export const FILE__OPEN_RECENT_SUBMENU = [...FILE__SKETCH_GROUP, '0_open_recent'];
// -- File / Sketchbook
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '1_sketchbook'];
// -- File / Examples
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples'];
export const EXAMPLES__BUILT_IN_GROUP = [...FILE__EXAMPLES_SUBMENU, '0_built_ins'];
export const EXAMPLES__ANY_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '1_any_board'];
export const EXAMPLES__CURRENT_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '2_current_board'];
export const EXAMPLES__USER_LIBS_GROUP = [...FILE__EXAMPLES_SUBMENU, '3_user_libs'];
// -- Edit
// `Copy`, `Copy to Forum`, `Paste`, etc.
// Note: `1_undo` is the first group from Theia, we start with `2`
@@ -30,10 +43,26 @@ export namespace ArduinoMenus {
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// Core settings, such as `Processor` and `Programmers` for the board.
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '1_board_settings'];
// `Board`, `Port`, and `Get Board Info`.
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// Context menu
// -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main'];
// `Find in reference`, `FAQ`, etc.
export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find'];
// `Advanced Mode`.
// XXX: this will be removed.
export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control'];
// `About` group
// XXX: on macOS, the about group is not under `Help`
export const HELP__ABOUT_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP), '999_about'];
// ------------
// Context menus
// -- Open
export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context'];
export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [...OPEN_SKETCH__CONTEXT, '0_open'];
@@ -50,3 +79,44 @@ export namespace ArduinoMenus {
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [...SKETCH_CONTROL__CONTEXT, '2_resources'];
}
/**
* This is a hack. It removes a submenu with all its children if any.
* Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299
*/
export function unregisterSubmenu(menuPath: string[], menuRegistry: MenuModelRegistry): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements MenuNode {
constructor(protected readonly menuPath: MenuPath, readonly label: string, protected options: SubMenuOptions = { order: '0' }) { }
get icon(): string | undefined {
return this.options?.iconClass;
}
get sortString(): string {
return this.options?.order || this.label;
}
get id(): string {
return [...this.menuPath, 'placeholder'].join('-');
}
}

View File

@@ -4,11 +4,12 @@ import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
import { NotificationCenter } from '../notification-center';
@injectable()
export class MonitorConnection {
@@ -25,8 +26,11 @@ export class MonitorConnection {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(MessageService)
protected messageService: MessageService;
@@ -110,11 +114,11 @@ export class MonitorConnection {
}
}
});
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.boardsServiceClient.onAttachedBoardsChanged(event => {
this.boardsServiceProvider.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.notificationCenter.onAttachedBoardsChanged(event => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
const { boardsConfig } = this.boardsServiceProvider;
if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
@@ -128,7 +132,7 @@ export class MonitorConnection {
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
});
@@ -154,7 +158,7 @@ export class MonitorConnection {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState.reachedState('started_contributions').then(() => {
const { boardsConfig } = this.boardsServiceClient;
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
@@ -227,7 +231,7 @@ export class MonitorConnection {
protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise<void> {
if (this.autoConnect) {
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then(ports => {

View File

@@ -2,7 +2,7 @@ import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MonitorConfig } from '../../common/protocol/monitor-service';
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
@injectable()
export class MonitorModel implements FrontendApplicationContribution {
@@ -12,8 +12,8 @@ export class MonitorModel implements FrontendApplicationContribution {
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly onChangeEmitter: Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>;
protected _autoscroll: boolean;

View File

@@ -5,7 +5,7 @@ import { OptionsType } from 'react-select/src/types';
import { isOSX } from '@theia/core/lib/common/os';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { DisposableCollection } from '@theia/core/lib/common/disposable'
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'
import { ReactWidget, Message, Widget, MessageLoop } from '@theia/core/lib/browser/widgets';
import { Board, Port } from '../../common/protocol/boards-service';
import { MonitorConfig } from '../../common/protocol/monitor-service';
@@ -45,10 +45,16 @@ export class MonitorWidget extends ReactWidget {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(Disposable.create(() => {
this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) {
this.monitorConnection.disconnect();
}
}));
}
@postConstruct()
@@ -73,10 +79,6 @@ export class MonitorWidget extends ReactWidget {
onCloseRequest(msg: Message): void {
this.closing = true;
this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) {
this.monitorConnection.disconnect();
}
super.onCloseRequest(msg);
}
@@ -100,6 +102,9 @@ export class MonitorWidget extends ReactWidget {
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
if (this.closing || !this.isAttached) {
return;
}
this.focusNode = element;
requestAnimationFrame(() => MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest));
}

View File

@@ -0,0 +1,98 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { NotificationServiceClient, NotificationServiceServer } from '../common/protocol/notification-service';
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
@injectable()
export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution {
@inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
protected readonly indexUpdatedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<void>();
protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{ config: Config | undefined }>();
protected readonly platformInstalledEmitter = new Emitter<{ item: BoardsPackage }>();
protected readonly platformUninstalledEmitter = new Emitter<{ item: BoardsPackage }>();
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[] }>();
protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter,
this.daemonStartedEmitter,
this.daemonStoppedEmitter,
this.configChangedEmitter,
this.platformInstalledEmitter,
this.platformUninstalledEmitter,
this.libraryInstalledEmitter,
this.libraryUninstalledEmitter,
this.attachedBoardsChangedEmitter
);
readonly onIndexUpdated = this.indexUpdatedEmitter.event;
readonly onDaemonStarted = this.daemonStartedEmitter.event;
readonly onDaemonStopped = this.daemonStoppedEmitter.event;
readonly onConfigChanged = this.configChangedEmitter.event;
readonly onPlatformInstalled = this.platformInstalledEmitter.event;
readonly onPlatformUninstalled = this.platformUninstalledEmitter.event;
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
@postConstruct()
protected init(): void {
this.server.setClient(this);
}
onStop(): void {
this.toDispose.dispose();
}
notifyIndexUpdated(): void {
this.indexUpdatedEmitter.fire();
}
notifyDaemonStarted(): void {
this.daemonStartedEmitter.fire();
}
notifyDaemonStopped(): void {
this.daemonStoppedEmitter.fire();
}
notifyConfigChanged(event: { config: Config | undefined }): void {
this.configChangedEmitter.fire(event);
}
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.platformInstalledEmitter.fire(event);
}
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.platformUninstalledEmitter.fire(event);
}
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.libraryInstalledEmitter.fire(event);
}
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.libraryUninstalledEmitter.fire(event);
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.attachedBoardsChangedEmitter.fire(event);
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.recentSketchesChangedEmitter.fire(event);
}
}

View File

@@ -0,0 +1,22 @@
import { inject, injectable } from 'inversify';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputService, OutputMessage } from '../common/protocol/output-service';
@injectable()
export class OutputServiceImpl implements OutputService {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
append(message: OutputMessage): void {
const { chunk } = message;
const channel = this.outputChannelManager.getChannel(`Arduino`);
channel.show({ preserveFocus: true });
channel.append(chunk);
}
}

View File

@@ -0,0 +1,791 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import { Disable } from 'react-disable';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { MaybePromise } from '@theia/core/lib/common/types';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { AbstractDialog, DialogProps, PreferenceService, PreferenceScope, DialogError, ReactWidget } from '@theia/core/lib/browser';
import { Index } from '../common/types';
import { ConfigService, FileSystemExt, Network, ProxySettings } from '../common/protocol';
export interface Settings extends Index {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: 'on' | 'off'; // `editor.autoSave`
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
checkForUpdates?: boolean; // `arduino.ide.autoUpdate`
verboseOnCompile: boolean; // `arduino.compile.verbose`
verboseOnUpload: boolean; // `arduino.upload.verbose`
verifyAfterUpload: boolean; // `arduino.upload.verify`
enableLsLogs: boolean; // `arduino.language.log`
sketchbookPath: string; // CLI
additionalUrls: string[]; // CLI
network: Network; // CLI
}
export namespace Settings {
export function belongsToCli<K extends keyof Settings>(key: K): boolean {
return key === 'sketchbookPath' || key === 'additionalUrls';
}
}
@injectable()
export class SettingsService {
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected ready = new Deferred<void>();
protected _settings: Settings;
@postConstruct()
protected async init(): Promise<void> {
await this.appStateService.reachedState('ready'); // Hack for https://github.com/eclipse-theia/theia/issues/8993
const settings = await this.loadSettings();
this._settings = deepClone(settings);
this.ready.resolve();
}
protected async loadSettings(): Promise<Settings> {
await this.preferenceService.ready;
const [
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
cliConfig
] = await Promise.all([
this.preferenceService.get<number>('editor.fontSize', 12),
this.preferenceService.get<string>('workbench.colorTheme', 'arduino-theme'),
this.preferenceService.get<'on' | 'off'>('editor.autoSave', 'on'),
this.preferenceService.get<boolean>('arduino.window.autoScale', true),
this.preferenceService.get<number>('arduino.window.zoomLevel', 0),
// this.preferenceService.get<string>('arduino.ide.autoUpdate', true),
this.preferenceService.get<boolean>('arduino.compile.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verify', true),
this.preferenceService.get<boolean>('arduino.language.log', true),
this.configService.getConfiguration()
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
additionalUrls,
sketchbookPath,
network
};
}
async settings(): Promise<Settings> {
await this.ready.promise;
return this._settings;
}
async update(settings: Settings, fireDidChange: boolean = false): Promise<void> {
await this.ready.promise;
for (const key of Object.keys(settings)) {
this._settings[key] = settings[key];
}
if (fireDidChange) {
this.onDidChangeEmitter.fire(this._settings);
}
}
async reset(): Promise<void> {
const settings = await this.loadSettings();
return this.update(settings, true);
}
async validate(settings: MaybePromise<Settings> = this.settings()): Promise<string | true> {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!await this.fileService.exists(new URI(sketchbookDir))) {
return `Invalid sketchbook location: ${sketchbookPath}`;
}
if (editorFontSize <= 0) {
return `Invalid editor font size. It must be a positive integer.`;
}
if (!ThemeService.get().getThemes().find(({ id }) => id === themeId)) {
return `Invalid theme.`;
}
return true;
} catch (err) {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
}
async save(): Promise<string | true> {
await this.ready.promise;
const {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
sketchbookPath,
additionalUrls,
network
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath)
]);
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
(config as any).network = network;
await Promise.all([
this.preferenceService.set('editor.fontSize', editorFontSize, PreferenceScope.User),
this.preferenceService.set('workbench.colorTheme', themeId, PreferenceScope.User),
this.preferenceService.set('editor.autoSave', autoSave, PreferenceScope.User),
this.preferenceService.set('arduino.window.autoScale', autoScaleInterface, PreferenceScope.User),
this.preferenceService.set('arduino.window.zoomLevel', interfaceScale, PreferenceScope.User),
// this.preferenceService.set('arduino.ide.autoUpdate', checkForUpdates, PreferenceScope.User),
this.preferenceService.set('arduino.compile.verbose', verboseOnCompile, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verbose', verboseOnUpload, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verify', verifyAfterUpload, PreferenceScope.User),
this.preferenceService.set('arduino.language.log', enableLsLogs, PreferenceScope.User),
this.configService.setConfiguration(config)
]);
this.onDidChangeEmitter.fire(this._settings);
return true;
}
}
export class SettingsComponent extends React.Component<SettingsComponent.Props, SettingsComponent.State> {
readonly toDispose = new DisposableCollection();
constructor(props: SettingsComponent.Props) {
super(props);
}
componentDidUpdate(_: SettingsComponent.Props, prevState: SettingsComponent.State): void {
if (this.state && prevState && JSON.stringify(this.state) !== JSON.stringify(prevState)) {
this.props.settingsService.update(this.state, true);
}
}
componentDidMount(): void {
this.props.settingsService.settings().then(settings => this.setState(settings));
this.toDispose.push(this.props.settingsService.onDidChange(settings => this.setState(settings)));
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
if (!this.state) {
return <div />;
}
return <Tabs>
<TabList>
<Tab>Settings</Tab>
<Tab>Network</Tab>
</TabList>
<TabPanel>
{this.renderSettings()}
</TabPanel>
<TabPanel>
{this.renderNetwork()}
</TabPanel>
</Tabs>;
}
protected renderSettings(): React.ReactNode {
return <div className='content noselect'>
Sketchbook location:
<div className='flex-line'>
<input
className='theia-input stretch'
type='text'
value={this.state.sketchbookPath}
onChange={this.sketchpathDidChange} />
<button className='theia-button shrink' onClick={this.browseSketchbookDidClick}>Browse</button>
</div>
<div className='flex-line'>
<div className='column'>
<div className='flex-line'>Editor font size:</div>
<div className='flex-line'>Interface scale:</div>
<div className='flex-line'>Theme:</div>
<div className='flex-line'>Show verbose output during:</div>
</div>
<div className='column'>
<div className='flex-line'>
<input
className='theia-input small'
type='number'
step={1}
pattern='[0-9]+'
onKeyDown={this.numbersOnlyKeyDown}
value={this.state.editorFontSize}
onChange={this.editorFontSizeDidChange} />
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoScaleInterface}
onChange={this.autoScaleInterfaceDidChange} />
Automatic
</label>
<input
className='theia-input small with-margin'
type='number'
step={20}
pattern='[0-9]+'
onKeyDown={this.noopKeyDown}
value={100 + this.state.interfaceScale * 20}
onChange={this.interfaceScaleDidChange} />
%
</div>
<div className='flex-line'>
<select
className='theia-select'
value={ThemeService.get().getThemes().find(({ id }) => id === this.state.themeId)?.label || 'Unknown'}
onChange={this.themeDidChange}>
{ThemeService.get().getThemes().map(({ id, label }) => <option key={id} value={label}>{label}</option>)}
</select>
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnCompile}
onChange={this.verboseOnCompileDidChange} />
compile
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnUpload}
onChange={this.verboseOnUploadDidChange} />
upload
</label>
</div>
</div>
</div>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verifyAfterUpload}
onChange={this.verifyAfterUploadDidChange} />
Verify code after upload
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.checkForUpdates}
onChange={this.checkForUpdatesDidChange}
disabled={true} />
Check for updates on startup
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoSave === 'on'}
onChange={this.autoSaveDidChange} />
Auto save
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.enableLsLogs}
onChange={this.enableLsLogsDidChange} />
Enable language server logging
</label>
<div className='flex-line'>
Additional boards manager URLs:
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.additionalUrls.join(',')}
onChange={this.additionalUrlsDidChange} />
<i className='fa fa-window-restore theia-button shrink' onClick={this.editAdditionalUrlDidClick} />
</div>
</div>;
}
protected renderNetwork(): React.ReactNode {
return <div className='content noselect'>
<form>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none'}
onChange={this.noProxyDidChange} />
No proxy
</label>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network !== 'none'}
onChange={this.manualProxyDidChange} />
Manual proxy configuration
</label>
</form>
{this.renderProxySettings()}
</div>;
}
protected renderProxySettings(): React.ReactNode {
const disabled = this.state.network === 'none';
return <Disable disabled={disabled}>
<div className='proxy-settings' aria-disabled={disabled}>
<form className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? true : this.state.network.protocol === 'http'}
onChange={this.httpProtocolDidChange} />
HTTP
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? false : this.state.network.protocol !== 'http'}
onChange={this.socksProtocolDidChange} />
SOCKS
</label>
</form>
<div className='flex-line proxy-settings'>
<div className='column'>
<div className='flex-line'>Host name:</div>
<div className='flex-line'>Port number:</div>
<div className='flex-line'>Username:</div>
<div className='flex-line'>Password:</div>
</div>
<div className='column stretch'>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.hostname}
onChange={this.hostnameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input small with-margin'
type='number'
pattern='[0-9]'
value={this.state.network === 'none' ? '' : this.state.network.port}
onKeyDown={this.numbersOnlyKeyDown}
onChange={this.portDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.username}
onChange={this.usernameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='password'
value={this.state.network === 'none' ? '' : this.state.network.password}
onChange={this.passwordDidChange} />
</div>
</div>
</div>
</div>
</Disable>;
}
private isControlKey(event: React.KeyboardEvent<HTMLInputElement>): boolean {
return !!event.key && ['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some(key => event.key.toLocaleLowerCase() === key);
}
protected noopKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
}
protected numbersOnlyKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
const key = Number(event.key)
if (isNaN(key) || event.key === null || event.key === ' ') {
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
return;
}
}
protected browseSketchbookDidClick = async () => {
const uri = await this.props.fileDialogService.showOpenDialog({
title: 'Select new sketchbook location',
openLabel: 'Chose',
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
this.setState({ sketchbookPath });
}
};
protected editAdditionalUrlDidClick = async () => {
const additionalUrls = await new AdditionalUrlsDialog(this.state.additionalUrls, this.props.windowService).open();
if (additionalUrls) {
this.setState({ additionalUrls });
}
};
protected editorFontSizeDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (value) {
this.setState({ editorFontSize: parseInt(value, 10) });
}
};
protected additionalUrlsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ additionalUrls: event.target.value.split(',').map(url => url.trim()) });
};
protected autoScaleInterfaceDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoScaleInterface: event.target.checked });
};
protected enableLsLogsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ enableLsLogs: event.target.checked });
};
protected interfaceScaleDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
const percentage = parseInt(value, 10);
if (isNaN(percentage)) {
return;
}
let interfaceScale = (percentage - 100) / 20;
if (!isNaN(interfaceScale)) {
this.setState({ interfaceScale });
}
};
protected verifyAfterUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verifyAfterUpload: event.target.checked });
};
protected checkForUpdatesDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ checkForUpdates: event.target.checked });
};
protected autoSaveDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoSave: event.target.checked ? 'on' : 'off' });
};
protected themeDidChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
}
};
protected verboseOnCompileDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnCompile: event.target.checked });
};
protected verboseOnUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnUpload: event.target.checked });
};
protected sketchpathDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const sketchbookPath = event.target.value;
if (sketchbookPath) {
this.setState({ sketchbookPath });
}
};
protected noProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: 'none' });
} else {
this.setState({ network: Network.Default() });
}
};
protected manualProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: Network.Default() });
} else {
this.setState({ network: 'none' });
}
};
protected httpProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks';
this.setState({ network });
}
};
protected socksProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks' : 'http';
this.setState({ network });
}
};
protected hostnameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.hostname = event.target.value;
this.setState({ network });
}
};
protected portDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.port = event.target.value;
this.setState({ network });
}
};
protected usernameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.username = event.target.value;
this.setState({ network });
}
};
protected passwordDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.password = event.target.value;
this.setState({ network });
}
};
private get cloneProxySettings(): ProxySettings {
const { network } = this.state;
if (network === 'none') {
throw new Error('Must be called when proxy is enabled.');
}
const copyNetwork = deepClone(network);
return copyNetwork;
}
}
export namespace SettingsComponent {
export interface Props {
readonly settingsService: SettingsService;
readonly fileService: FileService;
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
}
export interface State extends Settings { }
}
@injectable()
export class SettingsWidget extends ReactWidget {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(WindowService)
protected readonly windowService: WindowService;
protected render(): React.ReactNode {
return <SettingsComponent
settingsService={this.settingsService}
fileService={this.fileService}
fileDialogService={this.fileDialogService}
windowService={this.windowService} />;
}
}
@injectable()
export class SettingsDialogProps extends DialogProps {
}
@injectable()
export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
constructor(@inject(SettingsDialogProps) protected readonly props: SettingsDialogProps) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.settingsService.onDidChange(this.validate.bind(this)));
}
protected async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
}
return '';
}
get value(): Promise<Settings> {
return this.settingsService.settings();
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.settingsService.onDidChange(() => this.update()));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}
export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
protected readonly textArea: HTMLTextAreaElement;
constructor(urls: string[], windowService: WindowService) {
super({ title: 'Additional Boards Manager URLs' });
this.contentNode.classList.add('additional-urls-dialog');
const description = document.createElement('div');
description.textContent = 'Enter additional URLs, one for each row';
description.style.marginBottom = '5px';
this.contentNode.appendChild(description);
this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls.filter(url => url.trim()).filter(url => !!url).join('\n');
this.textArea.wrap = 'soft';
this.textArea.cols = 90;
this.textArea.rows = 5;
this.contentNode.appendChild(this.textArea);
const anchor = document.createElement('div');
anchor.classList.add('link');
anchor.textContent = 'Click for a list of unofficial board support URLs';
anchor.style.marginTop = '5px';
anchor.style.cursor = 'pointer';
this.addEventListener(
anchor,
'click',
() => windowService.openNewWindow('https://github.com/arduino/Arduino/wiki/Unofficial-list-of-3rd-party-boards-support-urls', { external: true })
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton('OK');
this.appendCloseButton('Cancel');
}
get value(): string[] {
return this.textArea.value.split('\n').map(url => url.trim());
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}
return false;
}
}

View File

@@ -135,18 +135,6 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
width: 740px;
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-foreground);
}
button.theia-button.main {
color: var(--theia-button-foreground);
}
.dialogControl {
margin: 0 20px 30px 0;
@@ -203,12 +191,14 @@ button.theia-button.main {
width: 100%;
overflow: hidden;
margin: 0px 3px 0px 3px;
border: 1px solid var(--theia-dropdown-border);
}
.arduino-boards-dropdown-list {
border: 3px solid var(--theia-activityBar-background);
margin: -3px;
margin: -1px;
z-index: 1;
border: 1px solid var(--theia-dropdown-border);
}
.arduino-boards-dropdown-item {
@@ -218,6 +208,7 @@ button.theia-button.main {
cursor: pointer;
color: var(--theia-foreground);
background: var(--theia-tab-unfocusedActiveBackground);
border: 1px solid var(--theia-tab-unfocusedActiveBackground);
}
.arduino-boards-dropdown-item .fa-check {
@@ -227,5 +218,5 @@ button.theia-button.main {
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-list-hoverBackground);
border: 1px solid var(--theia-focusBorder);
}

View File

@@ -0,0 +1,6 @@
/* Show the dirty indicator on unclosable widgets. On hover, it should still show the dot instead of the X. */
/* https://github.com/arduino/arduino-pro-ide/issues/380 */
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
background-size: 13px;
background-image: var(--theia-icon-circle);
}

View File

@@ -5,6 +5,8 @@
@import './arduino-select.css';
@import './status-bar.css';
@import './terminal.css';
@import './editor.css';
@import './settings-dialog.css';
.theia-input.warning:focus {
outline-width: 1px;
@@ -34,3 +36,28 @@
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
/* Overrule the default Theia CSS button styles. */
button.theia-button,
.theia-button {
border: 1px solid var(--theia-dropdown-border);
}
button.theia-button:hover,
.theia-button:hover {
border: 1px solid var(--theia-focusBorder);
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-foreground);
}
button.theia-button.main {
color: var(--theia-button-foreground);
}

View File

@@ -64,6 +64,23 @@
mask-position: 28px -4px;
}
.arduino-start-debug-icon {
-webkit-mask: url('../icons/debug-dark.svg') 50%;
mask: url('../icons/debug-dark.svg') 50%;
-webkit-mask-size: 100%;
mask-size: 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-ui-button-font-color);
}
.arduino-start-debug {
border-radius: 12px;
}
#arduino-toolbar-container {
display: flex;
width: 100%;

View File

@@ -1,8 +1,6 @@
.p-TabBar.theia-app-centers .p-TabBar-tabIcon.arduino-serial-monitor-tab-icon {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 41px;
background-position-x: 19px;
.monitor-tab-icon {
-webkit-mask: url('../icons/monitor-tab-icon.svg');
mask: url('../icons/monitor-tab-icon.svg');
}
.serial-monitor {

View File

@@ -0,0 +1,57 @@
.arduino-settings-dialog {
width: 740px;
}
.arduino-settings-dialog .content {
padding: 5px;
height: 250px;
}
.arduino-settings-dialog .flex-line {
display: flex;
align-items: center;
white-space: nowrap;
}
.arduino-settings-dialog .with-margin {
margin-left: 5px;
}
.arduino-settings-dialog .theia-select {
background: var(--theia-input-background) !important;
}
.arduino-settings-dialog .column > div {
height: 26px;
vertical-align: middle;
}
.arduino-settings-dialog .stretch {
width: 100% !important;
}
.arduino-settings-dialog .flex-line .theia-button.shrink {
min-width: unset;
}
.arduino-settings-dialog .proxy-settings {
margin: 5px;
}
.arduino-settings-dialog input[type="radio"] {
margin: 3px !important;
}
.arduino-settings-dialog .theia-input.small {
max-width: 50px;
width: 50px;
}
.additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground);
}
.arduino-settings-dialog .react-tabs__tab-list {
display: flex;
justify-content: center;
}

View File

@@ -1,25 +0,0 @@
import { injectable, inject, postConstruct } from 'inversify';
import { AboutDialog as TheiaAboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../../common/protocol/config-service';
@injectable()
export class AboutDialog extends TheiaAboutDialog {
@inject(ConfigService)
protected readonly configService: ConfigService;
@postConstruct()
protected async init(): Promise<void> {
const [, version] = await Promise.all([super.init(), this.configService.getVersion()]);
if (version) {
const { firstChild } = this.contentNode;
if (firstChild instanceof HTMLElement && firstChild.classList.contains(ABOUT_CONTENT_CLASS)) {
const cliVersion = document.createElement('div');
cliVersion.textContent = version;
firstChild.appendChild(cliVersion);
// TODO: anchor to the commit in the `arduino-cli` repository.
}
}
}
}

View File

@@ -2,31 +2,35 @@
import { injectable, inject } from 'inversify';
import { EditorWidget } from '@theia/editor/lib/browser';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { ConnectionStatusService, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service';
import { ApplicationShell as TheiaApplicationShell, Widget } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { EditorMode } from '../../editor-mode';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;
protected track(widget: Widget): void {
super.track(widget);
if (widget instanceof OutputWidget) {
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
}
if (!this.editorMode.proMode && widget instanceof EditorWidget) {
if (widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then(sketch => {
if (sketch) {
@@ -60,6 +64,10 @@ export class ApplicationShell extends TheiaApplicationShell {
}
async saveAll(): Promise<void> {
if (this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE) {
this.messageService.error('Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.');
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803
}
await super.saveAll();
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);

View File

@@ -19,7 +19,9 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.AUTO_SAVE,
CommonCommands.OPEN_PREFERENCES,
CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME
CommonCommands.SELECT_COLOR_THEME,
CommonCommands.ABOUT_COMMAND,
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877
]) {
registry.unregisterMenuAction(command);
}

View File

@@ -6,20 +6,30 @@ import {
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus
} from '@theia/core/lib/browser/connection-status-service';
import { ArduinoDaemonClientImpl } from '../../arduino-daemon-client-impl';
import { ArduinoDaemon } from '../../../common/protocol';
import { NotificationCenter } from '../../notification-center';
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected isRunning = false;
@postConstruct()
protected init(): void {
protected async init(): Promise<void> {
this.schedulePing();
try {
this.isRunning = await this.daemon.isRunning();
} catch { }
this.notificationCenter.onDaemonStarted(() => this.isRunning = true);
this.notificationCenter.onDaemonStopped(() => this.isRunning = false);
this.wsConnectionProvider.onIncomingMessageActivity(() => {
// natural activity
this.updateStatus(this.daemonClient.isRunning);
this.updateStatus(this.isRunning);
this.schedulePing();
});
}
@@ -29,22 +39,35 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected isRunning = false;
@postConstruct()
protected async init(): Promise<void> {
try {
this.isRunning = await this.daemon.isRunning();
} catch { }
this.notificationCenter.onDaemonStarted(() => this.isRunning = true);
this.notificationCenter.onDaemonStopped(() => this.isRunning = false);
}
protected onStateChange(state: ConnectionStatus): void {
if (!this.daemonClient.isRunning && state === ConnectionStatus.ONLINE) {
if (!this.isRunning && state === ConnectionStatus.ONLINE) {
return;
}
super.onStateChange(state);
}
protected handleOffline(): void {
const { isRunning } = this.daemonClient;
this.statusBar.setElement('connection-status', {
alignment: StatusBarAlignment.LEFT,
text: isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
tooltip: isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.',
text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
tooltip: this.isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.',
priority: 5000
});
this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement('connection-status')));

View File

@@ -1,15 +1,16 @@
import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CommandService } from '@theia/core/lib/common/command';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SketchesService } from '../../../common/protocol';
import { ArduinoCommands } from '../../arduino-commands';
@injectable()
export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(FileService)
protected readonly fileService: FileService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@@ -17,13 +18,17 @@ export class FrontendApplication extends TheiaFrontendApplication {
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
protected async initializeLayout(): Promise<void> {
await super.initializeLayout();
const roots = await this.workspaceService.roots;
for (const root of roots) {
const exists = await this.fileSystem.exists(root.uri);
const exists = await this.fileService.exists(root.resource);
if (exists) {
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.uri);
this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.resource);
}
}
}

View File

@@ -1,11 +1,11 @@
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets';
import { ILogger } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { EditorWidget } from '@theia/editor/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../../common/protocol/config-service';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@@ -20,7 +20,6 @@ export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration()
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));

View File

@@ -0,0 +1,15 @@
import { TabBar } from '@phosphor/widgets';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { TabBarRenderer as TheiaTabBarRenderer } from '@theia/core/lib/browser/shell/tab-bars';
export class TabBarRenderer extends TheiaTabBarRenderer {
createTabClass(data: TabBar.IRenderData<any>): string {
let className = super.createTabClass(data);
if (!data.title.closable && Saveable.isDirty(data.title.owner)) {
className += ' p-mod-closable';
}
return className;
}
}

View File

@@ -0,0 +1,14 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution, DebugMenus } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { unregisterSubmenu } from '../../menu/arduino-menus';
@injectable()
export class DebugFrontendApplicationContribution extends TheiaDebugFrontendApplicationContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
unregisterSubmenu(DebugMenus.DEBUG, registry);
}
}

View File

@@ -0,0 +1,45 @@
import { injectable } from 'inversify';
import { DebugError } from '@theia/debug/lib/common/debug-service';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
@injectable()
export class DebugSessionManager extends TheiaDebugSessionManager {
async start(options: DebugSessionOptions): Promise<DebugSession | undefined> {
return this.progressService.withProgress('Start...', 'debug', async () => {
try {
// Only save when dirty. To avoid saving temporary sketches.
// This is a quick fix for not saving the editor when there are no dirty editors.
// // https://github.com/bcmi-labs/arduino-editor/pull/172#issuecomment-741831888
if (this.shell.canSaveAll()) {
await this.shell.saveAll();
}
await this.fireWillStartDebugSession();
const resolved = await this.resolveConfiguration(options);
// preLaunchTask isn't run in case of auto restart as well as postDebugTask
if (!options.configuration.__restart) {
const taskRun = await this.runTask(options.workspaceFolderUri, resolved.configuration.preLaunchTask, true);
if (!taskRun) {
return undefined;
}
}
const sessionId = await this.debug.createDebugSession(resolved.configuration);
return this.doStart(sessionId, resolved);
} catch (e) {
if (DebugError.NotFound.is(e)) {
this.messageService.error(`The debug session type "${e.data.type}" is not supported.`);
return undefined;
}
this.messageService.error('There was an error starting the debug session, check the logs for more details.');
console.error('Error starting the debug session', e);
throw e;
}
});
}
}

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