Compare commits

..

333 Commits

Author SHA1 Message Date
Miro Spönemann
8cf019bc07 Updated CLI to v0.9.0 2020-02-26 18:17:37 +01:00
Miro Spönemann
ad4928957e Increased version number to 0.0.5 2020-02-26 17:32:30 +01:00
Roberto Sora
6f6f620ec9 Merge pull request #100 from bcmi-labs/debugger-cli
Debugger implementation based on arduino-cli debug command
2020-02-26 16:46:38 +01:00
Miro Spönemann
b055bd9e41 Workaround for Windows: We cannot use SIGINT to interrupt gdb, so kill the process on disconnect 2020-02-26 16:12:02 +01:00
Miro Spönemann
a6cef7c605 Detect even more error cases 2020-02-26 13:55:12 +01:00
Miro Spönemann
d56962251e Fixed: Commands that are registered to toolbar did not show up in Electron menu 2020-02-26 10:55:03 +01:00
Miro Spönemann
69c7383da8 Update Electron main menu when a toggle button is switched 2020-02-26 09:43:32 +01:00
Miro Spönemann
f6a8dceabc Improved error detection to catch more cases 2020-02-25 15:43:21 +01:00
Miro Spönemann
3ed72de810 Detect errors in spawned process 2020-02-25 14:08:08 +01:00
Miro Spönemann
2d9fa5615b Removed some unused stuff 2020-02-24 15:50:46 +01:00
Miro Spönemann
24bd23b669 arduino/arduino-pro-ide#188: Open Boards and Library manager on first start 2020-02-24 15:27:20 +01:00
Miro Spönemann
1bc996d8d8 Updated to CLI version 0.9.0-rc2 2020-02-24 15:26:35 +01:00
Miro Spönemann
beb529cf15 Removed unused CMSIS code 2020-02-24 15:05:09 +01:00
Miro Spönemann
a72533b208 Added basic support for global and static variables 2020-02-24 14:33:51 +01:00
Miro Spönemann
acf7b6a8da Removed workaround 2020-02-24 10:37:17 +01:00
Miro Spönemann
0445700088 Added 'optimize for debug' option 2020-02-24 10:35:36 +01:00
Miro Spönemann
486f110c80 Added initial commands 2020-02-20 16:53:50 +01:00
Miro Spönemann
a427cf94f1 Created new debug adapter that uses the arduino-cli debug command 2020-02-20 11:39:59 +01:00
Miro Spönemann
bd6a6382f6 Removed C/C++ config with hard-coded paths 2020-02-13 10:19:33 +01:00
Miro Spönemann
887682b9c3 Updated yarn.lock 2020-02-13 10:19:33 +01:00
jbicker
4c63af572e Added debug button to toolbar.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2020-02-13 10:15:12 +01:00
jbicker
40ddd3714b Added signal handlers to kill gdb server.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2020-02-13 10:15:12 +01:00
Miro Spönemann
2855026cec [debugger] Don't allow running more than one debug session 2020-02-13 10:15:12 +01:00
Miro Spönemann
879d5c7cc9 Fixed compile error 2020-02-13 10:15:12 +01:00
Miro Spönemann
8c3fab824f [debugger] Resolve URIs through FileSystem 2020-02-13 10:15:12 +01:00
Miro Spönemann
0f35821d14 Reuse spawnCommand util for more robust command execution 2020-02-13 10:15:12 +01:00
Miro Spönemann
fb50244a29 [debugger] Kill the gdb server on debug adapter process exit 2020-02-13 10:15:12 +01:00
Miro Spönemann
8a78e09c6d Improved error message when spawning commands fails with stderr output 2020-02-13 10:15:12 +01:00
Miro Spönemann
fe3cc1904c Don't use EventEmitter method 'off' introduced in NodeJS 10 (arduino/arduino-pro-ide#182) 2020-02-13 10:15:12 +01:00
Miro Spönemann
828379cdc9 Don't send 'terminated' on 'disconnect' (arduino/arduino-pro-ide#183) 2020-02-13 10:15:12 +01:00
jbicker
576f96a502 Validate workspace/current editor and open file on debug start
Tests whether the current workspace is a sketchfolder and
opens the respective ino file
or whether  the current editor is a ino file.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2020-02-13 10:15:12 +01:00
Miro Spönemann
2ba95947de [debug] Shut down previous session properly before starting a new one 2020-02-13 10:15:12 +01:00
Miro Spönemann
68db44fa49 [debugger] Convert URIs to paths 2020-02-13 10:15:12 +01:00
Miro Spönemann
36ac97d95d Improved creation of default debug configurations 2020-02-13 10:15:12 +01:00
Miro Spönemann
a886a106f5 Improved variable resolution and error handling 2020-02-13 10:15:12 +01:00
Miro Spönemann
1441b685ee Improved error reporting on launch 2020-02-13 10:15:12 +01:00
Miro Spönemann
38ab95973e Updated code to current version 2020-02-13 10:15:12 +01:00
Miro Spönemann
2ffca88c02 Updated versions and yarn.lock 2020-02-13 10:15:12 +01:00
Akos Kitta
2b7bceada0 Updated the packager, included the debug extension
Note, I did not check in `/electron/packager/yarn.lock`.
It should be checked in, see the `master`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-02-13 10:15:12 +01:00
Christian Weichel
5baf43bf25 Integrate with classic mode and start debugging immediately 2020-02-13 10:15:12 +01:00
Christian Weichel
68ff6acb6a [electron] Added debugger to Electron build 2020-02-13 10:15:12 +01:00
Christian Weichel
76f126b913 Make openocd stop properly 2020-02-13 10:15:12 +01:00
Christian Weichel
ec18cf0dc1 Cleanup 2020-02-13 10:15:12 +01:00
Christian Weichel
8aa356bd6e Automated debug config setup 2020-02-13 10:15:12 +01:00
Christian Weichel
ea5f528ef0 Actual debugging works 2020-02-13 10:15:12 +01:00
Christian Weichel
3d6d2ce814 Ran first debugging session 2020-02-13 10:15:12 +01:00
Christian Weichel
e189a8c33e Add theia/debug to build 2020-02-13 10:15:12 +01:00
Miro Spönemann
2f9ef76cd2 Updated yarn.lock 2020-02-13 07:56:25 +00:00
Akos Kitta
ef74b04b0b Merge pull request #98 from bcmi-labs/theme-update-vol2
Theme update vol2
2020-01-30 14:24:41 +01:00
Akos Kitta
4b859c2db9 Added a workaround for a memory leak.
eclipse-theia/theia#7008

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-01-30 11:31:38 +01:00
Akos Kitta
d53d474243 Disabled the window reload in the bundled app.
This change is a NOOP for the electron application started in dev mode.

Closes arduino/arduino-pro-ide#187

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-01-30 11:31:38 +01:00
Akos Kitta
403fdbfdc0 Removed a few custom colors and derived them from existing ones.
Fixed the color for the:
 - selected items in the Explorer,
 - warning `input`s,
 - set custom dialog background for the Arduino theme,
 - the query term matches in the command palette, and
 - react-select.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-01-30 11:31:32 +01:00
Jan Bicker
cbe603a99f Updated Arduino App to new theming system
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2020-01-29 11:08:53 +01:00
Miro Spönemann
1d98e79d7b [build] Updated to macOS 10.14 2020-01-27 08:50:14 +01:00
Luca Cipriani
41bf1ce6dc Merge pull request #97 from bcmi-labs/robust_workspace_init
More robust workspace initialization
2020-01-23 11:08:28 +00:00
Miro Spönemann
1aa944b25e More robust workspace initialization: guard against errors creating sketch dir 2020-01-20 16:48:24 +01:00
Miro Spönemann
b220ce4c5f Removed fs-extra dependency again, updated electron build resolution 2020-01-16 16:06:01 +01:00
Miro Spönemann
15c66442cc Use direct dependency instead of dep resolution for fs-extra 2020-01-16 15:31:30 +01:00
pirropirro
3a56c16ab2 Merge pull request #95 from bcmi-labs/pro-ide-142
Align language server spawning with arduino-cli
2020-01-16 11:36:28 +01:00
Miro Spönemann
cea62e315a Added unit test for getExecPath 2020-01-16 11:10:13 +01:00
Miro Spönemann
6618816330 Align language server spawning with arduino-cli 2020-01-16 09:39:43 +01:00
Miro Spönemann
2577451c15 Use latest version of fs-extra due to stack overflow bugs in earlier versions 2020-01-15 17:10:59 +01:00
pirropirro
a1ab42d282 Merge pull request #96 from bcmi-labs/pro-ide-21
Fallback rendering for Boards Manager
2020-01-14 16:27:03 +01:00
Miro Spönemann
8dbcb8bbb6 Fallback rendering in case a BoardPackage with no name or author is found (arduino/arduino-pro-ide#21) 2020-01-14 15:35:44 +01:00
pirropirro
1be76aa264 Merge pull request #93 from bcmi-labs/clangd_linux
Include clangd for Linux
2020-01-14 10:56:32 +01:00
Miro Spönemann
21cd580e41 Use zip target for Linux (see electron-userland/electron-builder#2868) 2020-01-13 09:44:06 +01:00
Miro Spönemann
8839e318f4 Merge pull request #92 from bcmi-labs/arduino-pro-ide_17
Updated keybindings
2020-01-13 09:14:24 +01:00
Miro Spönemann
bef9185c6c arduino/arduino-pro-ide#31: Include clangd for linux with 'bin' and 'lib' folders 2020-01-10 11:02:23 +01:00
Miro Spönemann
b8bd444def Updated keybindings (arduino/arduino-pro-ide#17), restore Output view on Verify / Upload 2020-01-09 16:22:03 +01:00
Miro Spönemann
b8fdb03433 Added script to configure node_gyp (nodejs/node-gyp#1933) 2020-01-08 13:09:22 +01:00
pirropirro
4290497edc Merge pull request #91 from bcmi-labs/fixed_cli_version
Use fixed version 0.7.1 of the cli to avoid future breakage
2020-01-07 11:02:05 +01:00
Miro Spönemann
7a89a8048f Use fixed version 0.7.1 of the cli to avoid future breakage 2020-01-07 10:49:36 +01:00
pirropirro
84be804df0 Update CLI config parsing (v0.7.0) 2020-01-07 10:44:54 +01:00
Miro Spönemann
f07441a7fc Updated clangd to version 9.0.0 2020-01-06 16:25:00 +01:00
Miro Spönemann
08f127711f bcmi-labs/arduino-language-server#17 (workaround for eclipse-theia/theia#6308) 2019-12-17 11:40:33 +01:00
Akos Kitta
d1ae15a838 Updated to the latest CLI config format.
See: arduino/arduino-cli@f2df284
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-12 15:11:02 +01:00
Akos Kitta
7ca8830a02 Bumped up from 0.0.3 to 0.0.4.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 16:23:33 +01:00
Akos Kitta
13b7a7e1d8 Ignore LS logs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 16:23:06 +01:00
Akos Kitta
c5c9b8674b Merge pull request #85 from bcmi-labs/arduino/arduino-pro-ide#112
Various serial-monitor fixes for 0.0.4: arduino/arduino-pro-ide#112
2019-12-11 16:18:46 +01:00
Akos Kitta
6447191bf5 [win] Implemented naive reconnecting.
For some reason, port is reported to be busy on Windows.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 09:40:41 +01:00
Akos Kitta
e78ed85761 [win]: Launch config fixes for Windows.
It is a workaround for eclipse-theia/theia#3590

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 14:37:10 +01:00
Akos Kitta
9bc520ccf9 Removed unused chance.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:54:01 +01:00
Akos Kitta
cfdb00529c Fixed focus.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:16 +01:00
Akos Kitta
8ccea24452 Let the serial monitor connect to unknown boards.
Fixes arduino/arduino-pro-ide#127

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:16 +01:00
Akos Kitta
ad563d26ba Re-enabled the build on this branch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:15 +01:00
Akos Kitta
f0a628534e Updated to 0.14.0-next.0159cd5b Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:05:55 +01:00
Akos Kitta
1b95242ad1 forward messages only when connected.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
729588770e fixed broken promise when reconnecting on baudRate changes.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
6b2046e090 Do not disconnect from the widget, it is done automatically.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
80673ad18f Styled the input.
Do not render the widget after a close request.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
2f33038695 No disconnect/reconnect when DNDing the widget.
- Updated to next Theia,
 - Added elecron launch config,
 - Yet another syling for the input + selects,
 - Close monitor connection on widget close not detach.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:28 +01:00
Akos Kitta
6154d1e8d5 aligned input to the selects.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
557ec2ae42 Wait until the boards config has been reset
from the local storage, then start the monitor connection.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
8c49c04359 🤞 finalized the monitor UI
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
29ebf055e6 Do not re-render the entire widget.
Otherwise the selects do not work.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
71842abfa3 Got rid of the send button, use keybinding instead
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
ed660ccd64 fixed the input focus when the view is activated
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
6af22ec9b8 aligned select height with Theia styles
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
79f588d067 Finalized the styling for react-select
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
e3e4a96db3 do not show border on focus.
when the list is opened, we have the accent color anyway.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
4129544738 generalized the react-select component
so tha we can reuse it all over the application.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
a866bde4d1 fixed background style when holding down select
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
92b6208a76 aligned core/lib input style with react-select
made the UI slicker by removing any non- :hover, :select borders.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
12deceef19 got rid of the border when not focused/selected
it does not remove the border but uses the background of the container.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
f635751a8c fix the react-select styling
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
85bf50213d Removed more logic from the widget.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
5aeb2d388e Flip the auto install buttons.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
b6b4c75718 CI skip.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
c4a8062df4 Updated to next Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:14 +01:00
Akos Kitta
6e89e89738 Fixed layout initialization.
Fixes for eclipse-theia/theia#6688

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
c7242ca34f slightly better reconnecting experience.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
a4e5e65286 simplified monitor connection API.
we have one connenction per editor anyways.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
80549db289 a few bugfixes. updated grpc dependencies.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
eb7b3ad683 Reconnect on interrupted system call.
However, we have to figure out why does it happen at all.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:53 +01:00
Akos Kitta
9efcbcf2ae [rewrite-me]: initial serial monitor changes
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:53 +01:00
Akos Kitta
d22c0b9e55 Merge pull request #90 from bcmi-labs/jx/update-theia
Update Theia and adjust Electron package name
2019-12-10 11:59:45 +01:00
Jan Keromnes
5d2f09354d Adjust Electron package 'name' and 'appId'. 2019-12-10 11:03:01 +01:00
Jan Keromnes
fcd6c792e3 Updated to next (0.14.0-next.0159cd5b) Theia.
Co-authored-by: Jan Keromnes <jan.keromnes@typefox.io>
Co-authored-by: Akos Kitta <kittaakos@typefox.io>

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 11:02:49 +01:00
Akos Kitta
94233a1a19 Merge pull request #80 from bcmi-labs/0.3.0-rc
All in on PR for 0.0.3
2019-12-09 09:58:59 +01:00
Jan Keromnes
7fb32766ca [doc] Fix a few typos 2019-12-07 09:59:45 +01:00
Akos Kitta
85cf8757c4 Merge pull request #89 from bcmi-labs/jx/document-boards-service
[doc] Document Arduino Pro IDE extension and a few services
2019-12-07 09:50:19 +01:00
Jan Keromnes
41c56c1126 [doc] Document Arduino Pro IDE extension and a few services 2019-12-06 16:03:58 +00:00
Jan Keromnes
4c503c0c5e Merge pull request #86 from bcmi-labs/jx/support-electron-dev
Support running the Electron app in Gitpod for faster code reviews
2019-12-06 11:10:03 +01:00
Jan Keromnes
3f180b6059 [minor] Make the README.md GIFs a bit smaller 2019-12-06 11:07:03 +01:00
Jan Keromnes
6a8a76f720 [minor] Fix project name in README.md 2019-12-06 11:04:01 +01:00
Jan Keromnes
9a27252d91 Support running the Electron app in Gitpod for faster code reviews 2019-12-05 10:26:53 +00:00
Akos Kitta
4e683b237d Made the editors closeable in pro mode.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 10:33:59 +01:00
Akos Kitta
a2a9cbb02e Fixed a styling issue in the boards select dialog.
Closes: arduino/arduino-pro-ide#137
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 09:18:46 +01:00
Akos Kitta
dd10436051 Fixed the Platform ordering.
We have to pick the installed version first.
Otherwise we lose the FQBN of the boards.
FBQN is used to check if a board has the corresponding core installed.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 09:10:01 +01:00
Akos Kitta
e79d42d6bd Updated to next (0.13.0-next.145f9137) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-25 09:16:22 +01:00
Akos Kitta
a9c9dcde7b Fixed a selection bug after installing a core/lib
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
62b18ccbed Fixed lib uninstall. Do not log it more than once.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
0a8b6bc41e Restart the LS on core install/remove.
Fixes bcmi-labs/arduino-language-server#9

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
b1388be5f9 Implemented uninstall.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
b4848f62fa Corrected the docs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f359843635 Fixed the version ordering for libs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
6448b447b3 Resolved a React warning.
Removed the widget update due to an incorrect manual merge.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
c7bb3abf19 Updated doc, code style changes.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
c3e2aa4feb Generalized the list item renderers.
To support update/downgrade.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
63cd2701b4 Simplified code when deceding which CLI to use.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
35ac73181b Enabled New Folder in classic mode.
-  Made sure the `Explorer` is visible before raising the input dialog.
 - Removed unused module.

Fixes arduino/arduino-pro-ide#84

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
840cde872c Made the editor mode dynamic.Signed-off-by: Akos Kitta <kittaakos@typefox.io> 2019-11-22 17:09:45 +01:00
Akos Kitta
c2008460b0 Fixed storeLayout when toggling the editor mode.
Workaround for eclipse-theia/theia#6579.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
435fdcdf7f Adjusted the version numbers.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
7e6343e60e Update the ListWidget on list container render.
To recalculate the desired size of the scroll-bar thumb.

arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
fdda4a72d0 Initial support for updating/downgrading cores.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
7077303a36 Hid the Explorer. Disabled new folder in workspace
when not in `pro-mode`.

Closes arduino/arduino-pro-ide#84.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
acd9bf1354 Fixed the editor restoration when toggling mode.
- Do not reset the layout. It messes up the editor's restoration.
 - Close all pro-mode only views when leaving pro-mode.
 - Initialize all views `onStart`. It is a NOOP in normal mode.

Closes arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
d92fc25769 Fixed endless loop in the sketch service.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f9a98d708e Removed an unused field.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
df33c5689f Gracefully handle disconnected frontends.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
2dc73eb3b5 Updated to next (0.13.0-next.83c59134) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f6444b2570 Update the ListWidget on list container render.
To recalculate the desired size of the scroll-bar thumb.

arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
186180800f Increased the z-index of the scroll-bar thumb
to make it visible even with the customized list item `background-color`

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Jan Keromnes
20bc3c6f13 Merge pull request #81 from bcmi-labs/choose-cli
Only pick `arduino-cli` from the PATH if it's more recent
2019-11-21 14:53:23 +01:00
Jan Keromnes
125ee70fa3 Sort package.json dependencies alphabetically 2019-11-21 10:37:14 +00:00
Jan Keromnes
3cfb1450c0 Only pick arduino-cli from the PATH if it's more recent 2019-11-21 10:37:14 +00:00
Akos Kitta
9643dd397f [doc]: Updated the CI section.
Documented when do we build the electron-based applications.
2019-11-21 11:16:52 +01:00
Akos Kitta
7c1ebf273c Merge pull request #75 from bcmi-labs/25-10-release
Various bug fixes, plus one or two enhancements.
2019-10-28 12:41:29 -04:00
Akos Kitta
05850b5f27 Ensure default data and sketch dir existence.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 15:13:53 +02:00
Akos Kitta
21dedd4b09 DROPME: disabled the uglification in the bundle.js
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 14:10:06 +02:00
Akos Kitta
90d7d88162 Updated the application name, version to 0.0.2.
Increased the heap size for the packager.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
3efb5a4e08 Switched from execFile to spawn.
So that we can add guards against whitespaces in the path.
Also cleaned up the code a bit.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
4353bfb5b9 Updated port discovery to support unknown boards
From now on, we do not retrieve the ports from the attached boards.
A board can be unknown but the port is still relevant.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
8971dc4c5f Implemented naive reconnecting.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2be54944bf Fixed the checkmark position in the dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2f84b5c6b7 Show the CLI version from the about dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
cebe15ef69 Removed duplicate log level from the log.
Print CLI version at start-up.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
1dda5dd95b Exposed version from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
de1caf1451 PROEDITOR-46: Added a core auto-installer.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
fb6785c5d3 PROEDITOR-53: Changed the way we set the workspace
Got rid of the `sketch` search parameter from the URL.

Rules:
 - Get the desired workspace location from the
  - `Path` defined as the `window.location.hash` of the URL,
  - most recent workspaces,
  - most recent sketches from the default sketch folder.
 - Validate the location.
 - If no valid location was found, create a new sketch in the default sketch folder.

Note: when validating the location of the workspace root, the root must always exist. However, when in pro-mode, the desired workspace root must
not be a sketch directory with the `.ino` file, but can be any existing location.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:24 +02:00
Akos Kitta
de1f341d19 Merge pull request #74 from bcmi-labs/remove-tab-context-menu-no-pro-mode
Removed the tab context menu when not in pro-mode.
2019-10-23 10:30:30 +02:00
Akos Kitta
817a28291b Removed the tab context menu when not in pro-mode.
When we are not in `pro-mode`, it does not make much sense to show the
context menu. It does not work anyways.

See: https://arduino.slack.com/archives/CJJHJCJSJ/p1569502896082800?thread_ts=1569489282.073200&cid=CJJHJCJSJ

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-23 09:57:56 +02:00
Akos Kitta
06ef598806 Merge pull request #72 from bcmi-labs/azure-service-connection-update
Switched to `typefox-service-account1`.
2019-10-17 10:00:27 +02:00
Akos Kitta
cc6a0ae212 Fixed various electron packager issues
- We have to reuse the `yarn.lock` file for the final app, not just
for the extensions.
 - Simplified the clean-up phase.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-17 08:45:26 +02:00
Akos Kitta
65a58ce2be Switched to typefox-service-account1.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-16 13:56:40 +02:00
Akos Kitta
6331b7ddfd Merge pull request #65 from bcmi-labs/increase_maxBuffer
Increase maxBuffer for spawned daemon
2019-10-07 16:57:25 +02:00
Miro Spönemann
f3667f0270 Merge pull request #67 from bcmi-labs/restartLSOnBoardChange
Restart the language server when the board is changed
2019-10-04 11:54:13 +02:00
Miro Spönemann
aa4f216544 Restart the language server when the board is changed 2019-10-02 13:16:10 +02:00
Akos Kitta
065f9f042b Merge pull request #66 from bcmi-labs/various-bug-fixes
Various bug fixes
2019-09-26 11:59:23 +02:00
Akos Kitta
765fcdfba7 Fixed the styles for the boards list dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:57 +02:00
Akos Kitta
476e658fea Cleaned up the menu contributions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:30 +02:00
Akos Kitta
a96ed31a45 Fixed the daemon log.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-25 16:34:42 +02:00
Miro Spönemann
ed4f23a32a Merge pull request #64 from bcmi-labs/msp_sendBoardConfig
Added JSON-RPC message to send the board configuration to the language server
2019-09-25 09:12:59 +02:00
Akos Kitta
4949df7395 Fixed missing New Sketch menu in electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 16:01:19 +02:00
Akos Kitta
55923be7fd PROEDITOR-48: Open last sketch at start-up
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 14:23:16 +02:00
Martino Facchin
2ef0d1d0db Increase maxBuffer for spawned daemon
When installed cores are A LOT, core listing needs a lot of space :)
2019-09-23 17:06:10 +02:00
Akos Kitta
7244694bd3 PROEDITOR-47: FIxed the visibility of close button
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 16:09:58 +02:00
Akos Kitta
d9e71c7e0d PROEDITOR-51: Fixed the styling for toggeled icons
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:39:15 +02:00
Akos Kitta
daedae1ba7 PROEDITOR-50: Customized the editor for built-ins
- From now on, sources from the `dataDir` opened in a read-only editor.
 - Disabled the tab-bar decoration for built-ins.
 - No problem markers for the built-ins.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:10:38 +02:00
Akos Kitta
ac4e877a10 Open More info in the default browser.
Instead of the Electron application.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-18 15:37:58 +02:00
Miro Spönemann
09243ff74d Added JSON-RPC message to send the board configuration to the language server 2019-09-18 09:46:07 +02:00
Miro Spönemann
5496edbb42 Merge pull request #62 from bcmi-labs/setup_cpp
Configure cpp extension to use embedded clangd
2019-09-16 13:09:23 +02:00
Miro Spönemann
62eff29172 Configure cpp extension to use embedded clangd 2019-09-16 08:42:30 +02:00
jbicker
2220e66f4b Adapted Dark, Light and Arduino themes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-11 11:55:29 +02:00
jbicker
c98ec29810 Merge branch 'add-cpp-extension' 2019-09-09 17:33:46 +02:00
jbicker
768958dfd5 Updated Yarn.lock
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-09 17:25:07 +02:00
Jan Bicker
8e747e19a6 CPP extension added. Theia updated.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-09-09 17:10:18 +02:00
Miro Spönemann
b1c69aef9f Merge pull request #59 from bcmi-labs/ino-language
Added language support for ino files
2019-09-09 10:28:40 +02:00
Miro Spönemann
ec6b5ed3f3 Added command line argument for specifying cli path 2019-09-06 14:15:59 +02:00
Miro Spönemann
60bf58ac0f Added download script for arduino-language-server 2019-09-06 14:15:59 +02:00
Miro Spönemann
c2675efea4 Fixed z-index of main toolbar 2019-09-06 14:15:59 +02:00
Miro Spönemann
7d04c7efb8 Added language contribution for ino files 2019-09-06 14:14:35 +02:00
Jan Bicker
fb542e2e40 Merge pull request #56 from bcmi-labs/advanced-mode-in-top-panel
Advanced mode in top panel
2019-09-06 13:08:18 +02:00
Jan Bicker
3e0842e93a Merge pull request #54 from bcmi-labs/monitor
Serial Monitor
2019-09-06 13:06:56 +02:00
Jan Bicker
90add23dae Merge pull request #61 from bcmi-labs/file-menu-fix
Fixed file menu
2019-09-06 13:05:18 +02:00
jbicker
6ff5405337 show advanced mode button in top panel
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-04 09:07:57 +02:00
jbicker
c564572718 Fixed file menu, New Sketch and New File items were not shown in main menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:44:50 +02:00
jbicker
79731304c1 Reestablish monitor connection after reloading the window, reconnect after selecting another board
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
2046c0bdee Continue monitoring with last connection if respective board is reconnected
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
3eebd580d8 Fixed wrong toppanel layout in electron
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
d8454456a9 Set focus on send field initially
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dac9c6437e Preserve baud rate and line ending; Use google protobuf Struct type now for setting configs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
7f33b62e0b Preserve Autoscroll and Timestamp states
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
459e55a69a Improved serial monitor dropdowns
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
f76f4543e9 Minor code improvements
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
9b255ac072 Timestamp formatted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
592086466c Clear send field after sending the value
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
c81ee1ede2 Arduino Toolbar Tooltips don't get clipped anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
6d2816a7f3 Added icon, changed appearance of active toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dd69092afd Added additional toolbar to the right of the toppanel.
Added Toolbar button for toggling serial monitor and tooltips for serial monitor toolbar items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
76d0f5a464 Implemented the Widget
Re-introduced bottom panel tabs

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:40:19 +02:00
Jan Bicker
206b65f138 Merge pull request #58 from bcmi-labs/use-sketch-as-ws
Use sketch folder as workspace
2019-09-03 16:32:08 +02:00
Akos Kitta
6d590cd111 Fixed the packager for the new folder structure.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 14:57:28 +02:00
Akos Kitta
e8e3c3dc4c Use the latest CLI, as stated in the doc.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 13:45:36 +02:00
Akos Kitta
e6e042c856 Moved System declaration before its use-site.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:41:05 +02:00
Akos Kitta
9298a8cc55 Log clean-up.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:26:13 +02:00
Akos Kitta
98764b56aa Switched to the JSON log format for the daemon.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:11:43 +02:00
Akos Kitta
9f7aec4091 Adapted to latest CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:52:56 +02:00
Akos Kitta
e636e06a7e Download today's CLI.
The issues was an interim CI problem.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:21:51 +02:00
Akos Kitta
c6311ecb1d Adapted the CLI download script.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:17:44 +02:00
Akos Kitta
cd94608aee Renamed the applications.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 08:05:47 +02:00
Jan Bicker
b82d5e4f0b Use sketch directory as url param when a new created sketch gets opened
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-28 08:13:11 +00:00
Akos Kitta
9ae721292d Fixed the FS path issue on Windows.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-27 18:27:12 +02:00
jbicker
41c603937c Setting download dir to {dataDir}/staging; Create data and sketch folders if they don't exist.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 16:53:59 +02:00
jbicker
d5589c435f Get the default sketchbook path from backend
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 15:59:18 +02:00
jbicker
d809daa20a Use sketch folder as workspace
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 13:20:01 +02:00
Akos Kitta
f9641a3d76 Initial support of the default paths from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-26 12:42:27 +02:00
Sven Efftinge
59553bf81f Merge pull request #55 from bcmi-labs/se/better-output
Better output format
2019-08-16 14:00:44 +02:00
Sven Efftinge
037efbaba2 Better output format 2019-08-15 11:50:50 +00:00
Luca Cipriani
a936e4c505 Merge pull request #49 from bcmi-labs/boards-manager
generalized the boards and the libraries views.
2019-08-14 17:34:55 +02:00
Akos Kitta
7c2a295631 Fixed bug when checking if a core is installed
`startsWith` was incorrect: `arduino:samd` Vs. `arduino:samd_beta`

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:40:39 +02:00
Akos Kitta
c5796677f8 Ignore whitespaces when searhing for libs, cores.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:39:02 +02:00
Akos Kitta
b6306c330f Update/download the library_index.json at start
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
692c3f6e3f Implemented serial-monitoring for the backend.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
8d79bb3ffb restored the the search limit for the lib manager.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
ec50ea673c do not await for attached boards.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
502e9042ad fine tuned selectedPort update
based on the detached boards.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
66f429c478 workaround for non-unique names.
Fine tuned the port unnselection when attached boards change.

This should make sure we do not have to `await` for the attached boards
from the backend.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
0dc45daf01 aligned list view styles.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
3fcf5a6ac9 generalized the boards and the libraries views.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
b24d440e22 Merge pull request #53 from bcmi-labs/PROEDITOR-27
PROEDITOR-27: Try to preserve the window.
2019-08-05 13:47:53 +02:00
Akos Kitta
af9b9fbeab Yet another attempt to fix timeout issues on Azure.
```
info There appears to be trouble with your network connection. Retrying...
error An unexpected error occurred: "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz: ESOCKETTIMEDOUT".
info If you think this is a bug, please open a bug report with the information provided in "D:\\a\\1\\s\\yarn-error.log".
```

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 13:17:59 +02:00
Stefania
37db6c4b43 Merge pull request #51 from bcmi-labs/search-in-workspace-and-git
Added "Search In Workspace" and "SCM/Git" Extensions.
2019-08-05 11:12:46 +02:00
Jan Bicker
9d5ad9b003 Added "Search In Workspace" and "SCM/Git" Extensions.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-05 09:58:52 +02:00
Akos Kitta
ded838b4e8 Fixed the preloadTemplate for electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:07 +02:00
Akos Kitta
82df8a6add PROEDITOR-27: Try to preserve the window.
When opening sketches. Rules:
 - If `sketch` is missing from the URL, we reuse the same window.
 - NOOP, if we try to open the currently opened sketch.
 - Otherwise, use the existing logic: open sketch in a new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:05 +02:00
Luca Cipriani
2914379586 Merge pull request #46 from bcmi-labs/poll-boards
Implemented the board discovery with polling.
2019-08-02 17:08:08 +02:00
Akos Kitta
6b25659fa6 Merge pull request #48 from bcmi-labs/monitoring-api
Updated the TS/JS API with the monitoring.
2019-07-30 13:04:46 +02:00
Akos Kitta
27dc6f9816 Updated the TS/JS API with the monitoring.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:57:23 +02:00
Akos Kitta
b78ddbeb64 Implemented the board discovery with polling.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:32:10 +02:00
jbicker
db78c8ac2d Minor css adjustments
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 12:00:26 +02:00
Luca Cipriani
95c5536060 Merge pull request #45 from bcmi-labs/new-file-menu-item
Added 'New File' Item in file menu
2019-07-26 11:37:48 +02:00
Akos Kitta
0aa34b1169 Added the Prerequisites section to the README.
Closes #44.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-26 11:25:40 +02:00
jbicker
b7d951b809 Added 'New File' Item in file menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 11:00:18 +02:00
Luca Cipriani
e11d9e0c78 Merge pull request #43 from bcmi-labs/toggle-pro-mode
One can toggle between pro and simple mode
2019-07-25 18:29:17 +02:00
jbicker
436e660d47 Avoid duplicate react keys in board select dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 18:04:59 +02:00
jbicker
23a967bd4c One can toggle between pro and simple mode
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 17:38:16 +02:00
Jan Bicker
17fab651e5 Merge pull request #41 from bcmi-labs/made-board-selector-async
Made boards selector async
2019-07-25 08:04:02 -07:00
Luca Cipriani
df7225c32b Merge pull request #29 from bcmi-labs/PROEDITOR-20
PROEDITOR-20: Restored the dirt flag in editor tab
2019-07-25 16:52:50 +02:00
Akos Kitta
69f63668b2 Merge pull request #42 from bcmi-labs/terminate-daemon
Fixed the daemon process termination.
2019-07-25 16:44:04 +02:00
Akos Kitta
d29ed24e49 Fixed the daemon process termination.
From now on, we spawn a detached process that
will periodically check whether the parent Theia
the process is alive, if no, terminates the daemon.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 16:11:40 +02:00
jbicker
a5294417c3 Made boards selector async
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 11:20:56 +02:00
Akos Kitta
83e966d208 Merge pull request #40 from bcmi-labs/arduino-cli
Switched to the official `arduino-cli`.
2019-07-25 09:57:13 +02:00
Luca Cipriani
5cc75118cd Merge pull request #30 from bcmi-labs/PROEDITOR-19
PROEDITOR-19: Open new sketches in new windows
2019-07-25 09:21:15 +02:00
Akos Kitta
19f058cb7f Run the Board.List requests sequentially.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 08:50:05 +02:00
Akos Kitta
7494beca33 Switched to the official arduino-cli.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 07:58:00 +02:00
Sven Efftinge
835107b5a6 [styling] use arduino theme background color from start 2019-07-24 15:00:01 +00:00
jbicker
4ced8237f7 Merge branch 'sketch-loader-bug-fix' 2019-07-24 16:50:52 +02:00
Jan Bicker
40eb74aeff Merge pull request #39 from bcmi-labs/sketch-loader-bug-fix-win
[win] Open sketch.
2019-07-24 07:48:50 -07:00
Jan Bicker
457b283ca5 Use theia main menu factory for electron again. 2019-07-24 07:47:02 -07:00
Akos Kitta
c0e279f3e8 [win] Open sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 21:35:06 +02:00
jbicker
75f7d3ca7c Fixed sketch service 2019-07-23 16:42:25 +02:00
Jan Bicker
cd8c138e1e Merge pull request #38 from bcmi-labs/status-bar-contribution
Added status bar contribution for line number and selected board.
2019-07-23 16:19:38 +02:00
jbicker
c0dea5c6b7 Added status bar contribution for line number and selected board.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-23 15:39:38 +02:00
Akos Kitta
c45e85eb89 PROEDITOR-19: Open new sketches in new windows
- From now on, each new sketch has its own window.
 - Fixed the `Download (of)? X completed` logging during index update.
 - Validate the file before opening the sketch in new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 08:25:49 +02:00
Akos Kitta
87cf5c6fd7 Merge pull request #34 from bcmi-labs/PROEDITOR-7
PROEDITOR-7: Cloned the Library Manager layout.
2019-07-22 16:46:18 +02:00
jbicker
0bfb4ba6ab Added noselect class to boards toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 16:08:29 +02:00
Akos Kitta
8d838fcce4 PROEDITOR-7: Cloned the Library Manager layout.
To match with the official Arduino editor's UI.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 15:52:08 +02:00
Akos Kitta
d6637c44e5 PROEDITOR-20: Restored the dirt flag in editor tab
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 14:19:49 +02:00
Luca Cipriani
cf44fe2ad0 Merge pull request #36 from bcmi-labs/board-select-dialog-ui-improvements
Improved layout of "Board Select" and "File Navigator" dialogs
2019-07-22 14:10:34 +02:00
jbicker
a6e1c4a93e Improved layout of "Board Select" and "File Navigator" dialogs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 14:09:55 +02:00
Luca Cipriani
269f08b74d Merge pull request #37 from bcmi-labs/board-selector-toolbar-item
Board selector toolbar item
2019-07-22 14:03:13 +02:00
jbicker
4d2bd87f74 Implemented custom dropdown for board selection
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 11:06:54 +02:00
jbicker
c2fbccc9e8 App doesn't show "open..." anymore if there are no sketches in default sketch folder.
Opens file navigator directly instead.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 13:49:28 +02:00
jbicker
6e0a0a19c5 Fixed Open Button
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 11:35:54 +02:00
jbicker
c88d8dd17b Small ui fix. Adapted css for browser menu active items.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 10:08:49 +02:00
Jan Bicker
7dfb28975e Merge pull request #35 from bcmi-labs/ui-improvements
Ui improvements
2019-07-18 09:59:10 +02:00
jbicker
2d4c710b6a Syntax coloring
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-17 18:40:48 +02:00
jbicker
dfc2ef967a Colors adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
jbicker
e2d3c7c1bd Made buttons bigger
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
Luca Cipriani
5a97a71d9b Merge pull request #32 from bcmi-labs/bring-back-menu
PROEDITOR-22: Brought back the menu.
2019-07-15 15:51:39 +02:00
jbicker
5e728523ef Changed layout of top panel in browser.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 13:12:33 +02:00
jbicker
2a0273c771 Brought back the menu.
With a restricted set of items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 12:53:17 +02:00
Luca Cipriani
35086ca1a1 Merge pull request #31 from bcmi-labs/open-boards-dialog
PROEDITOR-9: Open boards dialog
2019-07-15 12:44:16 +02:00
jbicker
23446284b7 Toggle the selected board in dropdown
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 17:48:20 +02:00
jbicker
89fb2fddbd Cache attached boards
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 11:50:44 +02:00
jbicker
a039597d40 Select Board Dialog Style and Layout
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-09 18:31:08 +02:00
jbicker
4429094139 Styling of select board and port dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 13:55:31 +02:00
jbicker
769689ff6d Added dialog implementation
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 11:58:59 +02:00
jbicker
4c66dec36e Added board select toolbar item
fill context menu with connected boards

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 14:08:45 +02:00
jbicker
ff336dccc5 Enhanced boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 09:03:26 +02:00
Jan Bicker
75ef8ea987 Merge pull request #27 from bcmi-labs/electron-4x-menu
Several UI enhancements
2019-06-26 09:02:14 +02:00
jbicker
1b90b7af29 Updated theia
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 08:43:38 +02:00
jbicker
c7c5ab57c2 Bugfix and yarn.lock update
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-25 17:37:58 +02:00
Akos Kitta
b7e17b5828 Updated to next (26c3273a) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-06-25 08:47:58 +02:00
jbicker
6195da3858 Added boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 18:00:17 +02:00
jbicker
c54ae96a3c Overwritten EditorWidgetFactory, made editor widget not closable.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 15:09:04 +02:00
jbicker
54d90d5413 Added save button and turned auto save on by default.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-21 13:11:16 +02:00
jbicker
f4f7a9998e Small fixes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 17:28:53 +02:00
jbicker
4096afde96 Open the sketch in a new window.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 09:35:56 +02:00
jbicker
92afa48c05 Implemented possibility to open sketches via file navigator.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 11:30:26 +02:00
jbicker
9d3cbf2ea0 Use service to load sketches
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 09:48:28 +02:00
jbicker
0c937212e2 Added Open Toolbutton
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-17 18:26:24 +02:00
Jan Bicker
7760915014 Merge pull request #23 from bcmi-labs/ui-changes
Ui changes
2019-06-17 09:36:26 +02:00
jbicker
f9746b350d Further customization of the statusbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 18:32:32 +02:00
jbicker
2066f20d78 Added the tooltip to the right of the toolbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 17:37:46 +02:00
jbicker
f5560626e5 Removed/hidden elements from bottom widget bar (Log output)
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 16:08:19 +02:00
jbicker
1f1861e1d8 Adapted design of toolbar.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 15:39:03 +02:00
jbicker
6a956afbcd Added toolbar to top panel.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 17:02:07 +02:00
jbicker
4c3becd3e8 Sidebar with Filenavigator, Librarylist and Boardlist does not get rendered initially anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 10:10:29 +02:00
jbicker
088951ae01 More color adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 08:30:59 +02:00
jbicker
b7cf9e6755 Removed menu, changed colors
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 07:55:58 +02:00
203 changed files with 30779 additions and 14438 deletions

7
.gitignore vendored
View File

@@ -2,11 +2,16 @@ node_modules/
# .node_modules is a hack for the electron builder.
.node_modules/
lib/
downloads/
build/
!electron/build/
src-gen/
arduino-ide-*/webpack.config.js
*webpack.config.js
.DS_Store
/workspace/static
.DS_Store
# switching from `electron` to `browser` in dev mode.
.browser_modules
# LS logs
inols*.log
yarn-error.log

View File

@@ -2,13 +2,17 @@ image:
file: Dockerfile
ports:
- port: 3000
onOpen: open-browser
- port: 3000
onOpen: open-preview
- port: 5900
onOpen: ignore
- port: 6080
onOpen: ignore
tasks:
- init: >
yarn &&
yarn --cwd ./arduino-ide-browser start
yarn --cwd ./browser-app start
github:
prebuilds:

139
.vscode/launch.json vendored
View File

@@ -1,32 +1,111 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Backend",
"program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js",
"${workspaceRoot}/arduino-ide-browser/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "Launch Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
},
{
"type": "node",
"request": "launch",
"name": "App (Electron)",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
]
}
},
"program": "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js",
"protocol": "inspector",
"args": [
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--remote-debugging-port=9222",
"--no-app-auto-install"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install"
],
"windows": {
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser - Debug CLI daemon)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--debug-cli=true"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
}
]
}

21
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"tslint.enable": true,
"tslint.configFile": "./tslint.json",
"editor.formatOnSave": true,
"files.exclude": {
"**/lib": false
},
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[typescript]": {
"editor.tabSize": 4
},
"[json]": {
"editor.tabSize": 2
},
"[jsonc]": {
"editor.tabSize": 2
},
"files.insertFinalNewline": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

16
.vscode/tasks.json vendored
View File

@@ -4,9 +4,9 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino-PoC - Start Browser Example",
"label": "Arduino Editor - Start Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser start",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
@@ -15,7 +15,7 @@
}
},
{
"label": "Arduino-PoC - Watch Theia Extension",
"label": "Arduino Editor - Watch Theia Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -26,9 +26,9 @@
}
},
{
"label": "Arduino-PoC - Watch Browser Example",
"label": "Arduino Editor - Watch Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser watch",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
@@ -37,11 +37,11 @@
}
},
{
"label": "Arduino-PoC - Watch All",
"label": "Arduino Editor - Watch All",
"type": "shell",
"dependsOn": [
"Arduino-PoC - Watch Theia Extension",
"Arduino-PoC - Watch Browser Example"
"Arduino Editor - Watch Theia Extension",
"Arduino Editor - Watch Browser Example"
]
}
]

View File

@@ -1,4 +1,4 @@
FROM gitpod/workspace-full
FROM gitpod/workspace-full-vnc
USER root
RUN apt-get update -q --fix-missing && \
@@ -7,7 +7,8 @@ RUN apt-get update -q --fix-missing && \
build-essential \
libssl-dev \
golang-go \
libxkbfile-dev
libxkbfile-dev \
libnss3-dev
RUN set -ex && \
tmpdir=$(mktemp -d) && \

View File

@@ -1,4 +1,4 @@
# Arduino IDE PoC
# Arduino Pro IDE
[![Build Status](https://dev.azure.com/typefox/Arduino/_apis/build/status/bcmi-labs.arduino-editor?branchName=master)](https://dev.azure.com/typefox/Arduino/_build/latest?definitionId=4&branchName=master)
@@ -9,12 +9,15 @@ It's built on top of a [fork of the arduino-cli](https://github.com/typefox/ardu
## How to try (offline)
### Prerequisites
You should be able to build Theia locally. The requirements are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites).
```
git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor
yarn
yarn rebuild:electron
yarn --cwd arduino-ide-electron start
yarn --cwd electron-app start
```
If you want to switch back to the browser-based example, execute the following in the repository root
@@ -23,13 +26,16 @@ yarn rebuild:browser
```
Then you can start the browser example again:
```
yarn --cwd arduino-ide-browser start
yarn --cwd browser-app start
```
## Arduino-PoC Electron Application
Click [here](./arduino-ide-extension/README.md) for more details on various IDE services, and the Arduino Pro IDE implementation in general.
## Arduino Pro IDE Electron Application
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
Currently, we provide the Arduino-PoC for the following platforms:
Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows,
- macOS, and
- Linux.
@@ -38,7 +44,7 @@ You can download the latest release applications fom [here](https://github.com/b
If you want to get a nightly build, go to the [Azure DevOps page](https://dev.azure.com/typefox/Arduino/_build?definitionId=4),
and follow the steps from below.
![](static/download_01.gif)
![](static/download_02.gif)
<img width="500" src="static/download_01.gif">
<img width="500" src="static/download_02.gif">
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.

View File

@@ -0,0 +1,40 @@
{
"name": "arduino-debugger-extension",
"version": "0.0.5",
"description": "An extension for debugging Arduino programs",
"license": "MIT",
"engines": {
"node": ">=10.10.0"
},
"dependencies": {
"@theia/debug": "next",
"arduino-ide-extension": "0.0.5",
"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"
},
"devDependencies": {
"rimraf": "^2.6.1",
"tslint": "^5.5.0",
"typescript": "3.5.1"
},
"files": [
"lib",
"src",
"build",
"data"
],
"theiaExtensions": [
{
"backend": "lib/node/backend-module",
"frontend": "lib/browser/frontend-module"
}
]
}

View File

@@ -0,0 +1,39 @@
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

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

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,4 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg width="28" height="28" viewBox="0 0 28 28" fill="#F6F6F6" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M15.1673 18.0687V23.0247C15.1673 23.5637 15.2723 24.5 14.7315 24.5H12.8328V23.3327H14V19.6122L13.7988 19.4022C13.0604 20.0803 12.1008 20.4669 11.0986 20.49C10.0964 20.5132 9.11994 20.1714 8.351 19.5282C7 18.1737 7.13826 16.3327 8.60475 14H4.66726V15.1672H3.50001V13.2685C3.50001 12.7277 4.43626 12.8327 4.97526 12.8327H9.76326L15.1673 18.0687ZM11.6673 5.83275H10.5V4.66725H12.775C13.3123 4.66725 14 4.9245 14 5.4635V9.366L14.8593 10.3862C14.927 9.83979 15.1906 9.33644 15.6013 8.96958C16.0119 8.60271 16.5416 8.39723 17.0923 8.39125C17.2298 8.37945 17.3684 8.39492 17.5 8.43675V5.83275H18.6673V8.88825C18.703 8.99154 18.7618 9.08536 18.8391 9.16265C18.9164 9.23995 19.0102 9.29871 19.1135 9.3345H22.1673V10.5H19.5615C19.593 10.5 19.6105 10.675 19.6105 10.85C19.6058 11.4034 19.4011 11.9365 19.0341 12.3508C18.6671 12.7651 18.1626 13.0326 17.6138 13.104L18.634 14H22.5383C23.0773 14 23.3345 14.6807 23.3345 15.225V17.5H22.1673V16.3327H19.2273L11.6673 8.98275V5.83275ZM14 0C11.2311 0 8.52431 0.821086 6.22202 2.35943C3.91973 3.89776 2.12532 6.08426 1.06569 8.64243C0.00606593 11.2006 -0.271181 14.0155 0.269012 16.7313C0.809205 19.447 2.14258 21.9416 4.10051 23.8995C6.05845 25.8574 8.55301 27.1908 11.2687 27.731C13.9845 28.2712 16.7994 27.9939 19.3576 26.9343C21.9157 25.8747 24.1022 24.0803 25.6406 21.778C27.1789 19.4757 28 16.7689 28 14C28 10.287 26.525 6.72601 23.8995 4.1005C21.274 1.475 17.713 0 14 0V0ZM25.6673 14C25.6692 16.6908 24.7364 19.2988 23.0283 21.378L6.622 4.97175C8.33036 3.57269 10.4009 2.68755 12.5927 2.41935C14.7845 2.15115 17.0074 2.51091 19.0027 3.45676C20.998 4.40262 22.6836 5.89567 23.8635 7.76217C25.0433 9.62867 25.6689 11.7919 25.6673 14ZM2.33276 14C2.33066 11.3091 3.26351 8.70111 4.97176 6.622L21.378 23.03C19.6693 24.4284 17.5987 25.313 15.407 25.5807C13.2153 25.8485 10.9926 25.4884 8.99754 24.5425C7.00244 23.5965 5.31693 22.1036 4.13708 20.2373C2.95722 18.3709 2.33152 16.208 2.33276 14Z" fill="white"/></g><defs><clipPath id="clip0"><rect width="28" height="28" fill="#F6F6F6"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,16 @@
.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

@@ -0,0 +1,89 @@
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 { ArduinoCli } from 'arduino-ide-extension/lib/node/arduino-cli';
@injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
readonly type = 'arduino';
readonly label = 'Arduino';
readonly languages = ['c', 'cpp', 'ino'];
@inject(ArduinoCli) arduinoCli: ArduinoCli;
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.arduinoCli.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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,140 @@
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

@@ -0,0 +1,71 @@
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,
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

@@ -0,0 +1,76 @@
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

@@ -0,0 +1,115 @@
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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,31 @@
{
"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

@@ -0,0 +1,37 @@
{
"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,38 +0,0 @@
{
"private": true,
"name": "arduino-ide-browser",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/editor": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1"
},
"devDependencies": {
"@theia/cli": "next"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace",
"watch": "theia build --watch --mode development"
},
"theia": {
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"defaultTheme": "arduino-theme"
}
}
}
}

View File

@@ -1,41 +0,0 @@
{
"private": true,
"name": "arduino-ide-electron",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/editor": "next",
"@theia/electron": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1"
},
"devDependencies": {
"@theia/cli": "next",
"electron": "^4.2.0"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace",
"watch": "theia build --watch --mode development"
},
"theia": {
"target": "electron",
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"defaultTheme": "arduino-theme"
}
}
}
}

View File

@@ -0,0 +1,52 @@
## 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.
### IDE Services
IDE services typically have a backend part in [src/node/](./src/node/) and a front-end part in [src/browser/](./src/browser/).
#### Boards Service
The Boards Service continuously checks the computer's ports, in order to detect when you connect or disconnect an Arduino board.
The Boards Manager lists all the known board types, and allows downloading new cores to get additional board types.
- [src/common/protocol/boards-service.ts](./src/common/protocol/boards-service.ts) implements the common classes and interfaces
- [src/node/boards-service-impl.ts](./src/node/boards-service-impl.ts) implements the service backend:
- discovering ports & boards
- searching for compatible board types
- installing new board types
- [src/browser/boards/boards-list-widget.ts](./src/browser/boards/boards-service-client-impl.ts) implements the Boards Manager front-end:
- browsing/searching available board types
- installing new board types
#### Core Service
The Core Service is responsible for building your sketches and uploading them to a board.
- [src/common/protocol/core-service.ts](./src/common/protocol/core-service.ts) implements the common classes and interfaces
- [src/node/core-service-impl.ts](./src/node/core-service-impl.ts) implements the service backend:
- compiling a sketch for a selected board type
- uploading a sketch to a connected board
#### Monitor Service
The Monitor Service allows getting information back from sketches running on your Arduino boards.
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
- [src/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
- connecting to / disconnecting from a board
- receiving and sending data
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
- viewing the output from a connected board
- entering data to send to the board
#### Config Service
The Config Service knows about your system, like for example the default sketch locations.
- [src/common/protocol/config-service.ts](./src/common/protocol/config-service.ts) implements the common classes and interfaces
- [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

View File

@@ -8,6 +8,113 @@
"name": "Ino",
"scopeName": "source.ino",
"patterns": [
{
"include": "#special_block"
},
{
"include": "#strings"
},
{
"match": "\\b(friend|explicit|virtual|override|final|noexcept)\\b",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(private:|protected:|public:)",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(catch|operator|try|throw|using)\\b",
"name": "keyword.control.cpp"
},
{
"match": "\\bdelete\\b(\\s*\\[\\])?|\\bnew\\b(?!])",
"name": "keyword.control.cpp"
},
{
"match": "\\b(f|m)[A-Z]\\w*\\b",
"name": "variable.other.readwrite.member.cpp"
},
{
"match": "\\bthis\\b",
"name": "variable.language.this.cpp"
},
{
"match": "\\bnullptr\\b",
"name": "constant.language.cpp"
},
{
"match": "\\btemplate\\b\\s*",
"name": "storage.type.template.cpp"
},
{
"match": "\\b(const_cast|dynamic_cast|reinterpret_cast|static_cast)\\b\\s*",
"name": "keyword.operator.cast.cpp"
},
{
"match": "::",
"name": "punctuation.separator.namespace.access.cpp"
},
{
"match": "\\b(and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq|alignof|alignas)\\b",
"name": "keyword.operator.cpp"
},
{
"match": "\\b(class|decltype|wchar_t|char16_t|char32_t)\\b",
"name": "storage.type.cpp"
},
{
"match": "\\b(constexpr|export|mutable|typename|thread_local)\\b",
"name": "storage.modifier.cpp"
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.prototype.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"include": "source.c"
},
{
"include": "#preprocessor-rule-enabled"
},
@@ -321,6 +428,19 @@
"match": "((\\.)|(->))\\s*(([a-zA-Z_][a-zA-Z_0-9]*)\\b(?!\\s*\\())?"
},
"block": {
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"name": "meta.block.cpp",
"patterns": [
{
"begin": "{",
@@ -341,6 +461,9 @@
"include": "#block_innards"
}
]
},
{
"include": "$base"
}
]
},
@@ -706,6 +829,69 @@
"include": "#line_continuation_character"
}
]
},
{
"begin": "(u|u8|U|L)?\"",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
}
},
"end": "\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
}
},
"name": "string.quoted.double.cpp",
"patterns": [
{
"match": "\\\\u\\h{4}|\\\\U\\h{8}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\['\"?\\\\abfnrtv]",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\[0-7]{1,3}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\x\\h+",
"name": "constant.character.escape.cpp"
},
{
"include": "source.c#string_placeholder"
}
]
},
{
"begin": "(u|u8|U|L)?R\"(?:([^ ()\\\\\\t]{0,16})|([^ ()\\\\\\t]*))\\(",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
},
"3": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"end": "\\)\\2(\\3)\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
},
"1": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"name": "string.quoted.double.raw.cpp"
}
]
},
@@ -1954,6 +2140,233 @@
"include": "#block_innards"
}
]
},
"angle_brackets": {
"begin": "<",
"end": ">",
"name": "meta.angle-brackets.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"include": "$base"
}
]
},
"constructor": {
"patterns": [
{
"begin": "(?x)\n(?:^\\s*) # beginning of line\n((?!while|for|do|if|else|switch|catch|enumerate|r?iterate)[A-Za-z_][A-Za-z0-9_:]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.constructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(:)\n(\n (?=\n \\s*[A-Za-z_][A-Za-z0-9_:]* # actual name\n \\s* (\\() # opening bracket\n )\n)",
"beginCaptures": {
"1": {
"name": "punctuation.definition.parameters.c"
}
},
"end": "(?=\\{)",
"name": "meta.function.constructor.initializer-list.cpp",
"patterns": [
{
"include": "$base"
}
]
}
]
},
"special_block": {
"patterns": [
{
"begin": "\\b(using)\\b\\s*(namespace)\\b\\s*((?:[_A-Za-z][_A-Za-z0-9]*\\b(::)?)*)",
"beginCaptures": {
"1": {
"name": "keyword.control.cpp"
},
"2": {
"name": "storage.type.cpp"
},
"3": {
"name": "entity.name.type.cpp"
}
},
"end": "(;)",
"name": "meta.using-namespace-declaration.cpp"
},
{
"begin": "\\b(namespace)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
}
},
"captures": {
"1": {
"name": "keyword.control.namespace.$2"
}
},
"end": "(?<=\\})|(?=(;|,|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.namespace-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(class|struct)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+(\\s*:\\s*(public|protected|private)\\s*([_A-Za-z][_A-Za-z0-9]*\\b)((\\s*,\\s*(public|protected|private)\\s*[_A-Za-z][_A-Za-z0-9]*\\b)*))?",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
},
"4": {
"name": "storage.type.modifier.cpp"
},
"5": {
"name": "entity.name.type.inherited.cpp"
},
"6": {
"patterns": [
{
"match": "(public|protected|private)",
"name": "storage.type.modifier.cpp"
},
{
"match": "[_A-Za-z][_A-Za-z0-9]*",
"name": "entity.name.type.inherited.cpp"
}
]
}
},
"end": "(?<=\\})|(?=(;|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.class-struct-block.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.cpp"
}
},
"end": "(\\})(\\s*\\n)?",
"endCaptures": {
"1": {
"name": "punctuation.section.block.end.bracket.curly.cpp"
},
"2": {
"name": "invalid.illegal.you-forgot-semicolon.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(extern)(?=\\s*\")",
"beginCaptures": {
"1": {
"name": "storage.modifier.cpp"
}
},
"end": "(?<=\\})|(?=\\w)|(?=\\s*#\\s*endif\\b)",
"name": "meta.extern-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}|(?=\\s*#\\s*endif\\b)",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
}
]
}
}
}

View File

@@ -1,38 +1,87 @@
{
"name": "arduino-ide-extension",
"version": "0.0.1",
"version": "0.0.5",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"engines": {
"node": ">=10.10.0"
},
"dependencies": {
"@grpc/grpc-js": "^0.4.0",
"@grpc/grpc-js": "^0.6.12",
"@theia/application-package": "next",
"@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/outline-view": "next",
"@theia/search-in-workspace": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"p-queue": "^5.0.0"
"@types/dateformat": "^3.0.1",
"@types/google-protobuf": "^3.7.1",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/which": "^1.3.1",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"google-protobuf": "^3.11.0",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-select": "^3.0.4",
"semver": "^6.3.0",
"string-natural-compare": "^2.0.3",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1"
},
"scripts": {
"generate-protoc": "./scripts/generate-protoc.sh && node ./scripts/patch-grpc-js.js",
"prepare": "yarn run clean && yarn run build",
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js",
"download-ls": "node ./scripts/download-ls.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && cp -rf src/node/cli-protocol lib/node && yarn lint",
"watch": "tsc -w"
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w",
"test": "mocha \"./test/**/*.test.ts\""
},
"mocha": {
"require": [
"ts-node/register",
"reflect-metadata/Reflect"
],
"reporter": "spec",
"colors": true,
"watch-extensions": "ts,tsx",
"timeout": 10000
},
"devDependencies": {
"@types/google-protobuf": "^3.2.7",
"grpc-tools": "^1.7.3",
"grpc_tools_node_protoc_ts": "^2.5.0",
"@types/chai": "^4.2.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc-tools": "^1.8.0",
"grpc_tools_node_protoc_ts": "^2.5.8",
"mocha": "^7.0.0",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"shelljs": "^0.8.3",
"ts-node": "^8.6.2",
"tslint": "^5.5.0",
"typescript": "2.9.1"
"typescript": "3.5.3",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
"files": [
"lib",
@@ -44,6 +93,10 @@
{
"backend": "lib/node/arduino-backend-module",
"frontend": "lib/browser/arduino-frontend-module"
},
{
"frontend": "lib/browser/menu/browser-arduino-menu-module",
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
}
]
}

View File

@@ -0,0 +1,61 @@
// @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.9.0'; // require('moment')().format('YYYYMMDD');
const path = require('path');
const shell = require('shelljs');
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 '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);
}
const url = `https://downloads.arduino.cc/arduino-cli${version.startsWith('nightly-') ? '/nightly' : ''}/arduino-cli_${version}_${suffix}`;
downloader.downloadUnzipFile(url, cli, 'arduino-cli', force);
})();

View File

@@ -0,0 +1,72 @@
// @ts-check
// The links to the downloads as of today (28.08.2019) are the following:
// - https://downloads.arduino.cc/arduino-language-server/nightly/arduino-language-server_${SUFFIX}
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
(() => {
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = '9.0.0';
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
choices: ['nightly'],
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
})
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['8.0.1', '9.0.0'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
})
.version(false).parse();
const alsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version']
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const alsTarget = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
let clangdTarget, alsSuffix, clangdSuffix;
switch (platform) {
case 'darwin':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
break;
case 'linux':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Linux_amd64.zip';
clangdSuffix = 'linux.zip'
break;
case 'win32':
clangdTarget = path.join(build, 'clangd.exe')
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
break;
}
if (!alsSuffix) {
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 clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.downloadUnzipAll(clangdUrl, build, clangdTarget, force);
})();

View File

@@ -0,0 +1,116 @@
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untargz = require('decompress-targz');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
});
process.on('uncaughtException', error => {
shell.echo(String(error));
shell.exit(1);
throw 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
*/
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}
/**
* @param url {string} Download URL
* @param targetDir {string} Directory into which to decompress the archive
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(targetDir)) {
if (shell.mkdir('-p', targetDir).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, targetDir, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}

View File

@@ -1,43 +0,0 @@
#!/bin/bash
WORKDIR=/tmp/arduino-cli-protoc
echo "Working in $WORKDIR"
# this could be a Git submodule, but that feels to clunky for just building the protobuf stuff
mkdir -p $WORKDIR
pushd $WORKDIR
if [ ! -d arduino-cli ]; then
git clone https://github.com/typefox/arduino-cli
cd arduino-cli
git checkout daemon
cd -
mkdir -p go/src/github.com/arduino
ln -s $PWD/arduino-cli go/src/github.com/arduino
export GOPATH=$PWD/go
cd go/src/github.com/arduino/arduino-cli
GOOS=linux go build -o arduino-cli.linux
# GOOS=darwin go build -o arduino-cli.darwin
fi
popd
# make sure the output path exists
mkdir -p src/node/cli-protocol
export PATH=$PATH:$PWD/node_modules/.bin
# generate js codes via grpc-tools
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./src/node/cli-protocol \
--grpc_out=./src/node/cli-protocol \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto
# generate d.ts codes
protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=./src/node/cli-protocol \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto

View File

@@ -0,0 +1,81 @@
// @ts-check
(async () => {
const DEFAULT_VERSION = 'nightly'; // '0.3.7-alpha.preview';
const os = require('os');
const path = require('path');
const { v4 } = require('uuid');
const shell = require('shelljs');
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
choices: [
// 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`.
'nightly'
],
describe: `The version of the 'arduino-cli' to download. Specify either an existing version or use 'nightly'. Defaults to ${DEFAULT_VERSION}.`
})
.version(false).parse();
const version = yargs['cli-version'];
if (version !== 'nightly') {
shell.echo(`Only 'nightly' version is supported.`);
shell.exit(1);
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino-cli.git ${repository}`).code !== 0) {
shell.exit(1);
}
if (version !== 'nightly') {
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
}
shell.echo('Generating TS/JS API from:');
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git -C ${repository} rev-parse --short HEAD`).code !== 0) {
shell.exit(1);
}
const pluginExec = shell.which('grpc_tools_node_protoc_plugin');
if (!pluginExec || pluginExec.code !== 0) {
shell.exit(1);
}
const plugin = pluginExec.stdout.trim();
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Generate JS code from the `.proto` files.
if (shell.exec(`grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${out} \
--grpc_out=${out} \
--plugin=protoc-gen-grpc=${plugin} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
// Generate the `.d.ts` files for JS.
if (shell.exec(`protoc \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', 'protoc-gen-ts')} \
--ts_out=${out} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
const { patch } = require('./patch-grpc-js');
patch([out])
shell.echo('Done.');
})();

View File

@@ -1,25 +1,38 @@
// Use `@grpc/grpc-js` instead of `grpc` at runtime.
// https://github.com/grpc/grpc-node/issues/624
(() => {
const fs = require('fs');
const path = require('path');
const roots = ['src']; // XXX: patch the `lib` instead?
console.info("🔧 >>> Patching code. Switching from 'grpc' to '@grpc/grpc-js'...");
for (const root of roots) {
const cliProtocolPath = path.resolve(__dirname, '..', root, 'node', 'cli-protocol');
for (const fileName of fs.readdirSync(cliProtocolPath)) {
const filePath = path.resolve(cliProtocolPath, fileName);
let content = fs.readFileSync(filePath, { encoding: 'utf8' });
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${filePath}.`);
fs.writeFileSync(filePath, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
}
content = fs.readFileSync(filePath, { encoding: 'utf8' });
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${filePath}.`);
fs.writeFileSync(filePath, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
// https://github.com/grpc/grpc-node/issues/931
const fs = require('fs');
const path = require('path');
module.exports.patch = function (roots = [path.join(__dirname, '..', 'src', 'node')]) {
console.info('🔧 <<< Patching code...');
patch(roots);
console.info('👌 <<< Done. The code has been patched.');
};
function patch(paths) {
for (const p of paths) {
const exist = fs.existsSync(p);
if (exist) {
const stat = fs.statSync(p);
if (stat.isDirectory()) {
console.info(`🔧 >>> Scanning code in ${p}...`);
patch(fs.readdirSync(p).map(name => path.join(p, name)));
} else {
let content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${p}.`);
fs.writeFileSync(p, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
}
content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${p}.`);
fs.writeFileSync(p, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
}
}
} else {
console.warn(`${p} does not exist. Skipping.`);
}
}
console.info('👌 <<< Done. The code has been patched.');
})();
}

View File

@@ -6,11 +6,38 @@ export namespace ArduinoCommands {
id: 'arduino-verify',
label: 'Verify Sketch'
}
export const VERIFY_TOOLBAR: Command = {
id: 'arduino-verify-toolbar',
}
export const UPLOAD: Command = {
id: 'arduino-upload',
label: 'Upload Sketch'
}
export const UPLOAD_TOOLBAR: Command = {
id: 'arduino-upload-toolbar',
}
export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
id: "arduino-toggle-compile-for-debug"
}
export const SHOW_OPEN_CONTEXT_MENU: Command = {
id: 'arduino-show-open-context-menu',
label: 'Open Sketch'
}
export const OPEN_FILE_NAVIGATOR: Command = {
id: 'arduino-open-file-navigator'
}
export const OPEN_SKETCH: Command = {
id: 'arduino-open-file'
}
export const SAVE_SKETCH: Command = {
id: 'arduino-save-file'
}
export const NEW_SKETCH: Command = {
id: "arduino-new-sketch",
@@ -18,9 +45,14 @@ export namespace ArduinoCommands {
category: 'File'
}
export const REFRESH_BOARDS: Command = {
id: "arduino-refresh-attached-boards",
label: "Refresh attached boards"
export const OPEN_BOARDS_DIALOG: Command = {
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,15 +0,0 @@
import { injectable } from "inversify";
import { MenuContribution, MenuModelRegistry } from "@theia/core";
import { CommonMenus } from "@theia/core/lib/browser";
import { ArduinoCommands } from "./arduino-commands";
@injectable()
export class ArduinoFileMenuContribution implements MenuContribution {
registerMenus(registry: MenuModelRegistry) {
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
})
}
}

View File

@@ -3,31 +3,69 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { DefaultFrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
import { ConnectedBoards } from './components/connected-boards';
import { CoreService } from '../common/protocol/core-service';
import { WorkspaceServiceExt } from './workspace-service-ext';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import {
ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
FrontendApplication, KeybindingContribution, KeybindingRegistry
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsNotificationService } from './boards-notification-service';
import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService } from '@theia/core';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { SketchFactory } from './sketch-factory';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { MaybePromise } from '@theia/core/lib/common/types';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from './editor-mode';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
}
export namespace ArduinoToolbarContextMenu {
export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu'];
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
}
@injectable()
export class ArduinoFrontendContribution extends DefaultFrontendApplicationContribution implements TabBarToolbarContribution, CommandContribution {
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution, ColorContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardService: BoardsService;
protected readonly boardsService: BoardsService;
@inject(CoreService)
protected readonly coreService: CoreService;
@@ -38,99 +76,253 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
@inject(ToolOutputServiceClient)
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
@inject(QuickPickService)
protected readonly quickPickService: QuickPickService;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected application: FrontendApplication;
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
@postConstruct()
protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind.
await this.workspaceServiceExt.roots();
const updateStatusBar = (config: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: BoardsConfig.Config.toString(config)
});
}
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClient.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
}
onStart(app: FrontendApplication): void {
this.application = app;
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(this.application);
}
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: ArduinoCommands.VERIFY.id,
command: ArduinoCommands.VERIFY.id,
tooltip: 'Verify',
group: 'arduino',
text: '$(check)'
command: ArduinoCommands.VERIFY_TOOLBAR.id,
tooltip: 'Verify'
});
registry.registerItem({
id: ArduinoCommands.UPLOAD.id,
command: ArduinoCommands.UPLOAD.id,
tooltip: 'Upload',
group: 'arduino',
text: '$(arrow-right)'
command: ArduinoCommands.UPLOAD_TOOLBAR.id,
tooltip: 'Upload'
});
registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID,
render: () => <ConnectedBoards
boardsService={this.boardService}
boardsNotificationService={this.boardsNotificationService}
quickPickService={this.quickPickService}
onNoBoardsInstalled={this.onNoBoardsInstalled.bind(this)}
onUnknownBoard={this.onUnknownBoard.bind(this)} />,
isVisible: widget => this.isArduinoEditor(widget)
})
id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
tooltip: 'Open',
priority: 2
});
registry.registerItem({
id: ArduinoCommands.SAVE_SKETCH.id,
command: ArduinoCommands.SAVE_SKETCH.id,
tooltip: 'Save',
priority: 2
});
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 2
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Toggle Serial Monitor'
});
registry.registerItem({
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id,
tooltip: 'Toggle Advanced Mode',
text: (this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)')
});
}
registerCommands(registry: CommandRegistry): void {
// TODO: use proper API https://github.com/eclipse-theia/theia/pull/6599
const allHandlers: { [id: string]: CommandHandler[] } = (registry as any)._handlers;
// Make sure to reveal the `Explorer` before executing `New File` and `New Folder`.
for (const command of [WorkspaceCommands.NEW_FILE, WorkspaceCommands.NEW_FOLDER]) {
const { id } = command;
const handlers = allHandlers[id].slice();
registry.unregisterCommand(id);
registry.registerCommand(command);
for (const handler of handlers) {
const wrapper: CommandHandler = {
execute: (...args: any[]) => {
this.fileNavigatorContributions.openView({ reveal: true }).then(() => handler.execute(args));
},
isVisible: (...args: any[]) => {
return handler.isVisible!(args);
},
isEnabled: (args: any[]) => {
return handler.isEnabled!(args);
},
isToggled: (args: any[]) => {
return handler.isToggled!(args);
}
};
if (!handler.isEnabled) {
delete wrapper.isEnabled;
}
if (!handler.isToggled) {
delete wrapper.isToggled;
}
if (!handler.isVisible) {
delete wrapper.isVisible;
}
registry.registerHandler(id, wrapper);
}
}
registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => this.isArduinoEditor(widget),
isEnabled: widget => this.isArduinoEditor(widget),
execute: async widget => {
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (!uri) {
return;
}
try {
await this.coreService.compile({ uri: uri.toString() });
} catch (e) {
await this.messageService.error(e.toString());
}
}
execute: this.verify.bind(this)
});
registry.registerCommand(ArduinoCommands.VERIFY_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: this.verify.bind(this)
});
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => {
this.editorMode.toggleCompileForDebug();
this.editorMode.menuContentChanged.fire();
},
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => this.isArduinoEditor(widget),
isEnabled: widget => this.isArduinoEditor(widget),
execute: async widget => {
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
execute: this.upload.bind(this)
});
registry.registerCommand(ArduinoCommands.UPLOAD_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: this.upload.bind(this)
});
const uri = this.toUri(widget);
if (!uri) {
return;
}
try {
await this.coreService.upload({ uri: uri.toString() });
} catch (e) {
await this.messageService.error(e.toString());
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (widget: Widget, target: EventTarget) => {
if (this.wsSketchCount) {
const el = (target as HTMLElement).parentElement;
if (el) {
this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, {
x: el.getBoundingClientRect().left,
y: el.getBoundingClientRect().top + el.offsetHeight
});
}
} else {
this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
}
}
});
registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, {
isEnabled: () => true,
execute: () => this.doOpenFile()
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
isEnabled: () => true,
execute: async (sketch: Sketch) => {
this.workspaceService.open(new URI(sketch.uri));
}
});
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (sketch: Sketch) => {
registry.executeCommand(CommonCommands.SAVE_ALL.id);
}
});
registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
execute: async uri => {
try {
@@ -139,42 +331,272 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
uri = uri.withPath(uri.path.dir.dir);
}
await this.sketchFactory.createNewSketch(uri);
const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
await this.messageService.error(e.toString());
}
}
}
}));
registry.registerCommand(ArduinoCommands.REFRESH_BOARDS, {
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
isEnabled: () => true,
execute: () => this.boardsNotificationService.notifyBoardsInstalled()
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.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()
});
}
protected async verify() {
const widget = this.getCurrentWidget();
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (!uri) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClient;
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 ${boardsConfig.selectedBoard.name}. Please install the board.`);
}
// Reveal the Output view asynchronously (don't await it)
this.outputContribution.openView({ reveal: true });
await this.coreService.compile({
uri: uri.toString(),
board: boardsConfig.selectedBoard,
optimizeForDebug: this.editorMode.compileForDebug
});
} catch (e) {
await this.messageService.error(e.toString());
}
}
protected async upload() {
const widget = this.getCurrentWidget();
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (!uri) {
return;
}
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
}
try {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
const { selectedPort } = boardsConfig;
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
// Reveal the Output view asynchronously (don't await it)
this.outputContribution.openView({ reveal: true });
await this.coreService.upload({
uri: uri.toString(),
board: boardsConfig.selectedBoard,
port: selectedPort.address,
optimizeForDebug: this.editorMode.compileForDebug
});
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
}
registerMenus(registry: MenuModelRegistry) {
if (!this.editorMode.proMode) {
// If are not in pro-mode, we have to disable the context menu for the tabs.
// Such as `Close`, `Close All`, etc.
for (const command of [
CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_RIGHT_TABS,
CommonCommands.CLOSE_ALL_TABS,
CommonCommands.COLLAPSE_PANEL,
CommonCommands.TOGGLE_MAXIMIZED,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
]) {
registry.unregisterMenuAction(command);
}
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS);
registry.unregisterMenuAction(WorkspaceCommands.CLOSE);
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW));
}
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.VERIFY.id,
label: 'Verify/Compile',
order: '2'
});
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.UPLOAD.id,
label: 'Upload',
order: '3'
});
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, {
commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id,
label: 'Open...'
});
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(CommonMenus.HELP, {
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
label: 'Advanced Mode'
});
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sketch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
});
}
protected getMenuId(menuPath: string[]): string {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: ArduinoCommands.VERIFY.id,
keybinding: 'ctrlcmd+alt+v'
});
keybindings.registerKeybinding({
command: ArduinoCommands.UPLOAD.id,
keybinding: 'ctrlcmd+alt+u'
});
}
protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
this.sketchService.getSketches().then(sketches => {
this.wsSketchCount = sketches.length;
sketches.forEach(sketch => {
const command: Command = {
id: 'openSketch' + sketch.name
}
this.commandRegistry.registerCommand(command, {
execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
});
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
commandId: command.id,
label: sketch.name
});
})
})
}
private async onNoBoardsInstalled() {
const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager");
if (!action) {
return;
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
for (const uri of uris) {
this.editorManager.open(new URI(uri));
}
});
}
private async onUnknownBoard() {
const action = await this.messageService.warn("There's a board connected for which you need to install software." +
" If this were not a PoC we would offer you the right package now.", "Open Boards Manager");
if (!action) {
return;
/**
* Opens a file after prompting the `Open File` dialog. Resolves to `undefined`, if
* - the workspace root is not set,
* - the file to open does not exist, or
* - it was not a file, but a directory.
*
* Otherwise, resolves to the URI of the file.
*/
protected async doOpenFile(): Promise<URI | undefined> {
const props: OpenFileDialogProps = {
title: WorkspaceCommands.OPEN_FILE.dialogLabel,
canSelectFolders: false,
canSelectFiles: true
};
const [rootStat] = await this.workspaceService.roots;
const destinationFileUri = await this.fileDialogService.showOpenDialog(props, rootStat);
if (destinationFileUri) {
const destinationFile = await this.fileSystem.getFileStat(destinationFileUri.toString());
if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile);
if (!message) {
this.workspaceService.open(destinationFileUri);
return destinationFileUri;
} else {
this.messageService.warn(message);
}
}
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
return undefined;
}
private isArduinoEditor(maybeEditorWidget: any): boolean {
if (maybeEditorWidget instanceof EditorWidget) {
return maybeEditorWidget.editor.uri.toString().endsWith('.ino');
protected getCurrentWidget(): EditorWidget | undefined {
let widget = this.editorManager.currentEditor;
if (!widget) {
const visibleWidgets = this.editorManager.all.filter(w => w.isVisible);
if (visibleWidgets.length > 0) {
widget = visibleWidgets[0];
}
}
return false;
return widget;
}
/**
* Returns `undefined` if the `file` is valid. Otherwise, returns with the validation error message.
*/
protected validate(file: FileStat): MaybePromise<string | undefined> {
const uri = new URI(file.uri);
const path = uri.path;
const { name, ext, dir } = path;
if (ext !== '.ino') {
return "Only sketches with '.ino' extension can be opened.";
}
if (name !== dir.name) {
return `The file "${name}${ext}" needs to be inside a sketch folder named "${name}".`;
}
return undefined;
}
private toUri(arg: any): URI | undefined {
@@ -190,4 +612,34 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
return undefined;
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
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.'
}
);
}
}

View File

@@ -5,14 +5,16 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'
import { FrontendApplicationContribution, FrontendApplication } 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 } from '../common/protocol/boards-service';
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -21,32 +23,89 @@ import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
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 { BoardsNotificationService } from './boards-notification-service';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { AWorkspaceService } from './arduino-workspace-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme';
import { ArduinoFileMenuContribution } from './arduino-file-menu';
import { MenuContribution } from '@theia/core';
import { SketchFactory } from './sketch-factory';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { SilentProblemContribution } from './customization/silent-problem-contribution';
import { ArduinoProblemContribution } from './customization/arduino-problem-contribution';
import { ArduinoNavigatorContribution } from './customization/arduino-navigator-contribution';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { ArduinoOutputToolContribution } from './customization/arduino-output-tool-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell, ShellLayoutRestorer, KeybindingContribution } from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import { ArduinoApplicationShell } from './customization/arduino-application-shell';
import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ArduinoScmContribution } from './customization/arduino-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { ArduinoSearchInWorkspaceContribution } from './customization/arduino-search-in-workspace-contribution';
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 } from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorModel } from './monitor/monitor-model';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ArduinoMonacoEditorProvider } from './editor/arduino-monaco-editor-provider';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './components/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';
const ElementQueries = require('css-element-queries/src/ElementQueries');
MonacoThemingService.register({
id: 'arduinoTheme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/arduino.color-theme.json')
});
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).to(ArduinoFileMenuContribution).inSingletonScope();
bind(KeybindingContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
// `ino` TextMate grammar
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);
@@ -56,12 +115,30 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
// Boards Notification service for updating boards list
// TODO (post-PoC): move this to boards service/backend
bind(BoardsNotificationService).toSelf().inSingletonScope();
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
bind(BoardsService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(BoardsServiceClientImpl);
return connection.createProxy(BoardsServicePath, client);
}).inSingletonScope();
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
bind(BoardsServiceClient).toDynamicValue(context => {
const client = context.container.get(BoardsServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
// boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget
bind(BoardsListWidget).toSelf();
@@ -72,6 +149,13 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}));
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: 'Select Board'
})
// Core service
bind(CoreService)
.toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath))
@@ -86,24 +170,75 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}).inSingletonScope();
// The workspace service extension
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt) => {
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt: WorkspaceServiceExt) => {
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
// Eagerly active the core, library, and boards services.
container.get(CoreService);
container.get(LibraryService);
container.get(BoardsService);
container.get(SketchesService);
return workspaceServiceExt;
});
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
bind(SketchFactory).toSelf().inSingletonScope();
// Serial Monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorWidget).toSelf();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget)
}));
// Frontend binding for the monitor service
bind(MonitorService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client);
}).inSingletonScope();
bind(MonitorConnection).toSelf().inSingletonScope();
// Monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient).toDynamicValue(context => {
const client = context.container.get(MonitorServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
return client;
}).inSingletonScope();
const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes);
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
// customizing default theia
unbind(OutlineViewContribution);
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
unbind(ProblemContribution);
bind(ProblemContribution).to(SilentProblemContribution).inSingletonScope();
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
rebind(OutlineViewContribution).to(ArduinoOutlineViewContribution).inSingletonScope();
rebind(ProblemContribution).to(ArduinoProblemContribution).inSingletonScope();
rebind(FileNavigatorContribution).to(ArduinoNavigatorContribution).inSingletonScope();
rebind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
rebind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
rebind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
rebind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope();
rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope();
rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Monaco customizations
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
rebind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// Decorator customizations
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
rebind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// Problem markers
bind(ArduinoProblemManager).toSelf().inSingletonScope();
rebind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
rebind(AboutDialog).toService(ArduinoAboutDialog);
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
bind(ArduinoShellLayoutRestorer).toSelf().inSingletonScope();
rebind(ShellLayoutRestorer).toService(ArduinoShellLayoutRestorer);
});

View File

@@ -1,27 +0,0 @@
import { Theme } from '@theia/core/lib/browser/theming';
import { MonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
const ARDUINO_CSS = require('../../src/browser/style/arduino.useable.css');
const ARDUINO_JSON = MonacoThemeRegistry.SINGLETON.register(
require('../../src/browser/data/arduino.color-theme.json'), {}, 'arduino', 'vs').name!;
export class ArduinoTheme {
static readonly arduino: Theme = {
id: 'arduino-theme',
label: 'Arduino Light Theme',
description: 'Arduino Light Theme',
editorTheme: ARDUINO_JSON,
activate() {
ARDUINO_CSS.use();
},
deactivate() {
ARDUINO_CSS.unuse();
}
}
static readonly themes: Theme[] = [
ArduinoTheme.arduino
]
}

View File

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

View File

@@ -1,50 +1,79 @@
import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service";
import { injectable, inject } from "inversify";
import { WorkspaceServer } from "@theia/workspace/lib/common";
import { FileSystem, FileStat } from "@theia/filesystem/lib/common";
import URI from "@theia/core/lib/common/uri";
import { SketchFactory } from "./sketch-factory";
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core';
import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService } from '../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { EditorMode } from './editor-mode';
/**
* This is workaround to have custom frontend binding for the default workspace, although we
* already have a custom binding for the backend.
*/
@injectable()
export class AWorkspaceService extends WorkspaceService {
export class ArduinoWorkspaceService extends WorkspaceService {
@inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected async getDefaultWorkspacePath(): Promise<string | undefined> {
let result = await super.getDefaultWorkspacePath();
if (!result) {
const userHome = await this.fileSystem.getCurrentUserHome();
if (!userHome) {
return;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(MessageService)
protected readonly messageService: MessageService;
private workspaceUri?: Promise<string | undefined>;
protected getDefaultWorkspaceUri(): Promise<string | undefined> {
if (this.workspaceUri) {
// Avoid creating a new sketch twice
return this.workspaceUri;
}
this.workspaceUri = (async () => {
try {
const hash = window.location.hash;
const [recentWorkspaces, recentSketches] = await Promise.all([
this.server.getRecentWorkspaces(),
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
]);
const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this)
}).resolve({ hash, recentWorkspaces, recentSketches });
if (toOpen) {
const { uri } = toOpen;
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
}
const { sketchDirUri } = (await this.configService.getConfiguration());
this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`)
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
} catch (err) {
this.logger.fatal(`Failed to determine the sketch directory: ${err}`)
this.messageService.error(
'There was an error creating the sketch directory. ' +
'See the log for more details. ' +
'The application will probably not work as expected.')
return super.getDefaultWorkspaceUri();
}
// The backend has created this location if it was missing.
result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString();
}
const stat = await this.fileSystem.getFileStat(result);
if (!stat) {
// workspace does not exist yet, create it
await this.fileSystem.createFolder(result);
await this.sketchFactory.createNewSketch(new URI(result));
}
return result;
})();
return this.workspaceUri;
}
protected async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> {
await super.setWorkspace(workspaceStat);
private async isValid(uri: string): Promise<boolean> {
const exists = await this.fileSystem.exists(uri);
if (!exists) {
return false;
}
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
if (this.editorMode.proMode) {
return true;
}
const sketchFolder = await this.sketchService.isSketchFolder(uri);
return sketchFolder;
}
}
}

View File

@@ -1,19 +0,0 @@
import { EventEmitter } from "events";
import { injectable } from "inversify";
// TODO (post-PoC): move this to the backend / BoardsService
@injectable()
export class BoardsNotificationService {
protected readonly emitter = new EventEmitter();
public on(event: 'boards-installed', listener: (...args: any[]) => void): this {
this.emitter.on(event, listener);
return this;
}
public notifyBoardsInstalled() {
this.emitter.emit('boards-installed');
}
}

View File

@@ -0,0 +1,63 @@
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 { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/progress-dialog';
import { BoardsConfig } from './boards-config';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
dialog.open();
try {
await this.boardsService.install({ item: candidate });
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
});
}
})
}
}
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
}
protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config);
}
protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
protected render(): React.ReactNode {
return <div className='selectBoardContainer'>
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@@ -0,0 +1,113 @@
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 { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {
}
@injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected config: BoardsConfig.Config = {};
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
super(props);
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => {
this.config = config;
this.update();
}));
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
]) {
const p = document.createElement('p');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
this.config = config;
this.update();
}));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return 'Please pick a board connected to the port you have selected.';
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
}

View File

@@ -0,0 +1,295 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
}
export interface State extends Config {
searchResults: Array<Board & { packageName: string }>;
knownPorts: Port[];
showAllPorts: boolean;
}
}
export abstract class Item<T> extends React.Component<{
item: T,
label: string,
selected: boolean,
onClick: (item: T) => void,
missing?: boolean,
detail?: string
}> {
render(): React.ReactNode {
const { selected, label, missing, detail } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing')
}
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
<div className='label'>
{label}
</div>
{!detail ? '' : <div className='detail'>{detail}</div>}
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
</div>;
}
protected onClick = () => {
this.props.onClick(this.props.item);
}
}
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceClient;
this.state = {
searchResults: [],
knownPorts: [],
showAllPorts: false,
...boardsConfig
}
}
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
const { boardsServiceClient: client } = this.props;
this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
})
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
const query = (typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase()
).trim();
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
}
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
});
}
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
const { boardsService } = this.props;
const query = (options.query || '').toLocaleLowerCase();
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
boardsService.search(options)
.then(({ items }) => items
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
.reduce((acc, curr) => acc.concat(curr), [])
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
.sort(Board.compare))
.then(searchResults => resolve({ searchResults }));
});
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected get availablePorts(): Promise<{ ports: Port[] }> {
return this.props.boardsService.getAvailablePorts();
}
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
return new Promise<{ knownPorts: Port[] }>(resolve => {
availablePorts
.then(({ ports }) => ports
.sort(Port.compare))
.then(knownPorts => resolve({ knownPorts }));
});
}
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
}
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
}
protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
}
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
}
render(): React.ReactNode {
return <div className='body'>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
</div>;
}
protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
return <div className='container'>
<div className='content'>
<div className='title'>
{title}
</div>
{contentRenderer()}
<div className='footer'>
{(footerRenderer ? footerRenderer() : '')}
</div>
</div>
</div>;
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults } = this.state;
// Board names are not unique. We show the corresponding core name as a detail.
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
const distinctBoardNames = new Map<string, number>();
for (const { name } of searchResults) {
const counter = distinctBoardNames.get(name) || 0;
distinctBoardNames.set(name, counter + 1);
}
// Due to the non-unique board names, we have to check the package name as well.
const selected = (board: Board & { packageName: string }) => {
if (!!selectedBoard) {
if (Board.equals(board, selectedBoard)) {
if ('packageName' in selectedBoard) {
return board.packageName === (selectedBoard as any).packageName;
}
return true;
}
}
return false;
}
return <React.Fragment>
<div className='search'>
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
key={`${board.name}-${board.packageName}`}
item={board}
label={board.name}
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
selected={selected(board)}
onClick={this.selectBoard}
missing={!Board.installed(board)}
/>)}
</div>
</React.Fragment>;
}
protected renderPorts(): React.ReactNode {
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
const ports = this.state.knownPorts.filter(filter);
return !ports.length ?
(
<div className='loading noselect'>
No ports discovered
</div>
) :
(
<div className='ports list'>
{ports.map(port => <Item<Port>
key={Port.toString(port)}
item={port}
label={Port.toString(port)}
selected={Port.equals(this.state.selectedPort, port)}
onClick={this.selectPort}
/>)}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return <div className='noselect'>
<label
title='Shows all available ports when enabled'>
<input
type='checkbox'
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
</label>
</div>;
}
}
export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.is(other)) {
return !!selectedBoard
&& Board.equals(other, selectedBoard)
&& Port.sameAs(selectedPort, other.port);
}
return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return left.selectedBoard === right.selectedBoard
&& left.selectedPort === right.selectedPort;
}
export function toString(config: Config, options: { default: string } = { default: '' }): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
}
}

View File

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
super({
id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemRenderer
});
}
}

View File

@@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class BoardsListWidget extends ListWidget {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
protected widgetProps(): ListWidget.Props {
return {
id: BoardsListWidget.WIDGET_ID,
title: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip'
}
}
}

View File

@@ -0,0 +1,178 @@
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 { LocalStorageService } 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, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
@inject(ILogger)
protected logger: ILogger;
@inject(MessageService)
protected messageService: MessageService;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
* We have to listen on such changes and auto-reconnect the same board on another port.
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
async onStart(): Promise<void> {
return this.loadState();
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
this.boardsConfig = {
selectedBoard,
selectedPort: undefined
};
}
// Try to reconnect.
this.tryReconnect(attached.boards, attached.ports);
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
this.boardsConfig = this.latestValidBoardsConfig;
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
};
return true;
}
}
}
return false;
}
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardUninstalledEmitter.fire(event);
}
set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
}
get boardsConfig(): BoardsConfig.Config {
return this._boardsConfig;
}
/**
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
*/
canVerify(
config: BoardsConfig.Config | undefined = this.boardsConfig,
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
if (!config) {
return false;
}
if (!config.selectedBoard) {
if (!options.silent) {
this.messageService.warn('No boards selected.', { timeout: 3000 });
}
return false;
}
return true;
}
/**
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
*/
canUploadTo(
config: BoardsConfig.Config | undefined = this.boardsConfig,
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
if (!this.canVerify(config, options)) {
return false;
}
const { name } = config.selectedBoard;
if (!config.selectedPort) {
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.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
}
return false;
}
return true;
}
protected saveState(): Promise<void> {
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
if (this.canUploadTo(this.latestValidBoardsConfig)) {
this.boardsConfig = this.latestValidBoardsConfig;
}
}
}
}

View File

@@ -0,0 +1,202 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config';
export interface BoardsDropDownListCoords {
readonly top: number;
readonly left: number;
readonly width: number;
readonly paddingTop: number;
}
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) {
super(props);
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
list.id = 'boards-dropdown-container';
document.body.appendChild(list);
this.dropdownElement = list;
}
}
render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
protected renderNode(): React.ReactNode {
const { coords, items } = this.props;
if (coords === 'hidden') {
return '';
}
items.push({
label: 'Select Other Board & Port',
selected: false,
onClick: () => this.props.openBoardsConfig()
})
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
...coords
}}>
{items.map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
</div>
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
coords: 'hidden'
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
]);
Promise.all([
boardService.getAttachedBoards(),
boardService.getAvailablePorts()
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
this.setState({ attachedBoards, availablePorts })
});
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height
}
});
} else {
this.setState({ coords: 'hidden'});
}
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is)
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
label: `${board.name} at ${board.port}`,
selected: configuredBoard === board,
onClick: () => {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
}
}
}));
return <React.Fragment>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={title}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{title}
</div>
</div>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
};
}

View File

@@ -1,20 +1,14 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
async initializeLayout(): Promise<void> {
await this.openView();
}
}
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
constructor() {
super({
@@ -24,9 +18,22 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
area: 'left',
rank: 600
},
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleKeybinding: 'ctrlcmd+shift+b'
});
}
async initializeLayout(): Promise<void> {
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...'
});
}
}
}

View File

@@ -1,87 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
const boardsServiceDelegate = this.boardsService;
const boardsService: BoardsService = {
getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(),
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
return <FilterableListContainer
service={boardsService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import Select from 'react-select';
import { Styles } from 'react-select/src/styles';
import { Props } from 'react-select/src/components';
import { ThemeConfig } from 'react-select/src/theme';
export class ArduinoSelect<T> extends Select<T> {
constructor(props: Readonly<Props<T>>) {
super(props);
}
render(): React.ReactNode {
const controlHeight = 27; // from `monitor.css` -> `.serial-monitor-container .head` (`height: 27px;`)
const styles: Styles = {
control: styles => ({
...styles,
minWidth: 120,
color: 'var(--theia-foreground)'
}),
dropdownIndicator: styles => ({
...styles,
padding: 0
}),
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: () => ({
padding: '0px 5px'
}),
menu: styles => ({
...styles,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight,
baseUnit: 2,
menuGutter: 4
}, colors: {
...theme.colors,
// `primary50`??? it's crazy but apparently, without this, we would get a light-blueish
// color when selecting an option in the select by clicking and then not releasing the button.
// https://react-select.com/styles#overriding-the-theme
primary50: 'var(--theia-list-activeSelectionBackground)',
}
});
const DropdownIndicator = () => <span className='fa fa-caret-down caret' />;
return <Select
{...this.props}
components={{ DropdownIndicator }}
theme={theme}
styles={styles}
classNamePrefix='arduino-select'
isSearchable={false}
/>
}
}

View File

@@ -1,78 +1,66 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
export class ComponentListItem<T extends ArduinoComponent> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
private onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.props.windowService.openNewWindow(target.href);
event.nativeEvent.preventDefault();
constructor(props: ComponentListItem.Props<T>) {
super(props);
if (props.item.installable) {
const version = props.item.availableVersions.filter(version => version !== props.item.installedVersion)[0];
this.state = {
selectedVersion: version
};
}
}
private async install(item: ArduinoComponent) {
await this.props.install(item);
protected async install(item: T): Promise<void> {
const toInstall = this.state.selectedVersion;
const version = this.props.item.availableVersions.filter(version => version !== this.state.selectedVersion)[0];
this.setState({
selectedVersion: version
});
try {
await this.props.install(item, toInstall);
} catch {
this.setState({
selectedVersion: toInstall
});
}
}
protected async uninstall(item: T): Promise<void> {
await this.props.uninstall(item);
}
protected onVersionChange(version: Installable.Version) {
this.setState({ selectedVersion: version });
}
render(): React.ReactNode {
const { item } = this.props;
const style = ComponentListItem.Styles;
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className={style.VERSION_INFO_CLASS}>
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
</div>;
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const install = this.props.install && item.installable && !item.installedVersion &&
<button className={style.INSTALL_BTN_CLASS} onClick={this.install.bind(this, item)}>INSTALL</button>;
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
<div className={style.HEADER_CLASS}>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className={style.CONTENT_CLASS}>
{summary}
</div>
<div className={style.FOOTER_CLASS}>
{moreInfo}
{install}
</div>
</div>;
const { item, itemRenderer } = this.props;
return itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
);
}
}
export namespace ComponentListItem {
export interface Props {
readonly item: ArduinoComponent;
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
export interface Props<T extends ArduinoComponent> {
readonly item: T;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
}
export namespace Styles {
export const LIST_ITEM_CLASS = 'component-list-item';
export const HEADER_CLASS = 'header';
export const VERSION_INFO_CLASS = 'version-info';
export const CONTENT_CLASS = 'content';
export const FOOTER_CLASS = 'footer';
export const INSTALLED_CLASS = 'installed';
export const NO_SELECT_CLASS = 'noselect';
export const NAME_CLASS = 'name';
export const AUTHOR_CLASS = 'author';
export const VERSION_CLASS = 'version';
export const SUMMARY_CLASS = 'summary';
export const DESCRIPTION_CLASS = 'description';
export const INSTALL_BTN_CLASS = 'install';
export interface State {
selectedVersion?: Installable.Version;
}
}

View File

@@ -1,24 +1,51 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentListItem } from './component-list-item';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentList extends React.Component<ComponentList.Props> {
export class ComponentList<T extends ArduinoComponent> extends React.Component<ComponentList.Props<T>> {
protected container?: HTMLElement;
render(): React.ReactNode {
return <div>
{this.props.items.map(item => <ComponentListItem key={item.name} item={item} windowService={this.props.windowService} install={this.props.install} />)}
return <div
className={'items-container'}
ref={this.setRef}>
{this.props.items.map(item => this.createItem(item))}
</div>;
}
componentDidMount(): void {
if (this.container && this.props.resolveContainer) {
this.props.resolveContainer(this.container);
}
}
protected setRef = (element: HTMLElement | null) => {
this.container = element || undefined;
}
protected createItem(item: T): React.ReactNode {
return <ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall} />
}
}
export namespace ComponentList {
export interface Props {
readonly items: ArduinoComponent[];
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
export interface Props<T extends ArduinoComponent> {
readonly items: T[];
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
}
}

View File

@@ -1,67 +1,119 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentList } from './component-list';
import { SearchBar } from './search-bar';
import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { InstallationProgressDialog } from '../installation-progress-dialog';
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
constructor(props: Readonly<FilterableListContainer.Props>) {
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
super(props);
this.state = {
filterText: '',
items: []
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
}
componentWillMount(): void {
componentDidMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
}
componentDidUpdate(): void {
// See: arduino/arduino-pro-ide#101
// Resets the top of the perfect scroll-bar's thumb.
this.props.container.updateScrollBar();
}
render(): React.ReactNode {
return <div className={FilterableListContainer.Styles.FILTERABLE_LIST_CONTAINER_CLASS}>
<SearchBar
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
/>
<ComponentList
items={this.state.items}
install={this.install.bind(this)}
windowService={this.props.windowService}
/>
return <div className={'filterable-list-container'}>
{this.renderSearchFilter()}
{this.renderSearchBar()}
{this.renderComponentList()}
</div>
}
private handleFilterTextChange(filterText: string): void {
this.props.service.search({ query: filterText }).then(result => {
protected renderSearchFilter(): React.ReactNode {
return undefined;
}
protected renderSearchBar(): React.ReactNode {
return <SearchBar
resolveFocus={this.props.resolveFocus}
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
/>
}
protected renderComponentList(): React.ReactNode {
const { itemLabel, resolveContainer, itemRenderer } = this.props;
return <ComponentList<T>
items={this.state.items}
itemLabel={itemLabel}
itemRenderer={itemRenderer}
install={this.install.bind(this)}
uninstall={this.uninstall.bind(this)}
resolveContainer={resolveContainer}
/>
}
protected handleFilterTextChange = (filterText: string) => {
this.setState({ filterText });
this.search(filterText);
}
protected search(query: string): void {
const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => {
const { items } = result;
this.setState({
filterText,
items: this.sort(items)
});
});
}
protected sort(items: ArduinoComponent[]): ArduinoComponent[] {
return items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name === b.name) {
return 0;
} else {
return 1;
}
});
protected sort(items: T[]): T[] {
const { itemLabel } = this.props;
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
}
protected async install(comp: ArduinoComponent): Promise<void> {
const dialog = new InstallationProgressDialog(comp.name);
protected async install(item: T, version: Installable.Version): Promise<void> {
const { installable, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item), version);
dialog.open();
try {
await this.props.service.install(comp);
const { items } = await this.props.service.search({ query: this.state.filterText });
await installable.install({ item, version });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
}
}
protected async uninstall(item: T): Promise<void> {
const uninstall = await new ConfirmDialog({
title: 'Uninstall',
msg: `Do you want to uninstall ${item.name}?`,
ok: 'Yes',
cancel: 'No'
}).open();
if (!uninstall) {
return;
}
const { installable, searchable, itemLabel } = this.props;
const dialog = new UninstallationProgressDialog(itemLabel(item));
dialog.open();
try {
await installable.uninstall({ item });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
@@ -72,24 +124,20 @@ export class FilterableListContainer extends React.Component<FilterableListConta
export namespace FilterableListContainer {
export interface Props {
readonly service: ComponentSource;
readonly windowService: WindowService;
export interface Props<T extends ArduinoComponent> {
readonly container: ListWidget<T>;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string>;
}
export interface State {
export interface State<T> {
filterText: string;
items: ArduinoComponent[];
}
export namespace Styles {
export const FILTERABLE_LIST_CONTAINER_CLASS = 'filterable-list-container';
}
export interface ComponentSource {
search(req: { query: string }): Promise<{ items: ArduinoComponent[] }>
install(board: ArduinoComponent): Promise<void>;
items: T[];
}
}

View File

@@ -0,0 +1,102 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
@injectable()
export class ListItemRenderer<T extends ArduinoComponent> {
@inject(WindowService)
protected windowService: WindowService;
protected onMoreInfoClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
event.nativeEvent.preventDefault();
}
}
renderItem(
input: ComponentListItem.State & { item: T },
install: (item: T) => Promise<void>,
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
const { item } = input;
let nameAndAuthor: JSX.Element;
if (item.name && item.author) {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
nameAndAuthor = <span>{name} by {author}</span>
} else if (item.name) {
nameAndAuthor = <span className='name'>{item.name}</span>;
} else if ((item as any).id) {
nameAndAuthor = <span className='name'>{(item as any).id}</span>;
} else {
nameAndAuthor = <span className='name'>Unknown</span>;
}
const onClickUninstall = () => uninstall(item);
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed' onClick={onClickUninstall} />
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>More info</a>;
const onClickInstall = () => install(item);
const installButton = item.installable &&
<button className='theia-button install' onClick={onClickInstall}>INSTALL</button>;
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const version = event.target.value;
if (version) {
onVersionChange(version);
}
}
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select
className='theia-select'
value={input.selectedVersion}
onChange={onSelectChange}>
{
item.availableVersions
.filter(version => version !== item.installedVersion) // Filter the version that is currently installed.
.map(version => <option value={version} key={version}>{version}</option>)
}
</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
{nameAndAuthor}
{installedVersion}
</div>
<div className='content'>
{summary}
{description}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@@ -0,0 +1,13 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget';
@injectable()
export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable';
import { Searchable } from '../../../common/protocol/searchable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
@injectable()
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<string>();
constructor(protected options: ListWidget.Options<T>) {
super();
const { id, label, iconClass } = options;
this.id = id;
this.title.label = label;
this.title.caption = label;
this.title.iconClass = iconClass
this.title.closable = true;
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = {
suppressScrollX: true
}
this.toDispose.push(this.filterTextChangeEmitter);
}
@postConstruct()
protected init(): void {
this.update();
}
protected getScrollContainer(): MaybePromise<HTMLElement> {
return this.deferredContainer.promise;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
render(): React.ReactNode {
return <FilterableListContainer<T>
container={this}
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
searchable={this.options.searchable}
installable={this.options.installable}
itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}/>;
}
refresh(filterText: string): void {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
}
updateScrollBar(): void {
if (this.scrollBar) {
this.scrollBar.update();
}
}
}
export namespace ListWidget {
export interface Options<T extends ArduinoComponent> {
readonly id: string;
readonly label: string;
readonly iconClass: string;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
}
}

View File

@@ -9,15 +9,22 @@ export class SearchBar extends React.Component<SearchBar.Props> {
render(): React.ReactNode {
return <input
className={SearchBar.Styles.SEARCH_BAR_CLASS}
ref={this.setRef}
className={`theia-input ${SearchBar.Styles.SEARCH_BAR_CLASS}`}
type='text'
placeholder='Search'
placeholder='Filter your search...'
size={1}
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>;
}
private setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
}
private handleFilterTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.props.onFilterTextChanged(event.target.value);
}
@@ -29,6 +36,7 @@ export namespace SearchBar {
export interface Props {
filterText: string;
onFilterTextChanged(filterText: string): void;
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
}
export namespace Styles {

View File

@@ -1,147 +0,0 @@
import * as React from 'react';
import { BoardsService, Board } from '../../common/protocol/boards-service';
// import { SelectBoardDialog } from './select-board-dialog';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsNotificationService } from '../boards-notification-service';
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
static TOOLBAR_ID: 'connected-boards-toolbar';
constructor(props: ConnectedBoards.Props) {
super(props);
this.state = { boardsLoading: false };
props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled());
}
render(): React.ReactNode {
let content = [];
if (!!this.state.boards && this.state.boards.length > 0) {
content = this.state.boards.map((b, i) => <option value={i} key={i}>{b.name}</option>);
} else {
let label;
if (this.state.boardsLoading) {
label = "Loading ...";
} else {
label = "No board attached";
}
content = [ <option key="loading" value="0">{label}</option> ];
}
return <div className={ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}>
<select disabled={!this.state.boards}
onChange={this.onBoardSelect.bind(this)}
value={this.state.selection}>
<optgroup label="Attached boards">
{ content }
</optgroup>
<optgroup label="_________">
{ !!this.state.otherBoard && <option value="selected-other" key="selected-other">{this.state.otherBoard.name} (not attached)</option> }
<option value="select-other" key="select-other">Select other Board</option>
</optgroup>
</select>
</div>;
}
componentDidMount(): void {
this.reloadBoards();
}
protected onBoardsInstalled() {
if (!!this.findUnknownBoards()) {
this.reloadBoards();
}
}
protected findUnknownBoards(): Board[] {
if (!this.state || !this.state.boards) {
return [];
}
return this.state.boards.filter(b => !b.fqbn || b.name === "unknown");
}
protected async reloadBoards() {
const prevSelection = this.state.selection;
this.setState({ boardsLoading: true, boards: undefined, selection: "loading" });
const { boards } = await this.props.boardsService.getAttachedBoards()
this.setState({ boards, boardsLoading: false, selection: prevSelection });
if (boards) {
this.setState({ selection: "0" });
await this.props.boardsService.selectBoard(boards[0]);
const unknownBoards = this.findUnknownBoards();
if (unknownBoards && unknownBoards.length > 0) {
this.props.onUnknownBoard(unknownBoards[0]);
}
}
}
protected async onBoardSelect(evt: React.ChangeEvent<HTMLSelectElement>) {
const selection = evt.target.value;
if (selection === "select-other" || selection === "selected-other") {
let selectedBoard = this.state.otherBoard;
if (selection === "select-other" || !selectedBoard) {
selectedBoard = await this.selectedInstalledBoard();
}
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({otherBoard: selectedBoard, selection: "selected-other"});
return;
}
const selectedBoard = (this.state.boards || [])[parseInt(selection, 10)];
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({selection});
}
protected async selectedInstalledBoard(): Promise<Board | undefined> {
const {items} = await this.props.boardsService.search({});
const idx = new Map<string, Board>();
items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) ));
if (idx.size === 0) {
this.props.onNoBoardsInstalled();
return;
}
const selection = await this.props.quickPickService.show(Array.from(idx.keys()));
if (!selection) {
return;
}
return idx.get(selection);
}
}
export namespace ConnectedBoards {
export interface Props {
readonly boardsService: BoardsService;
readonly boardsNotificationService: BoardsNotificationService;
readonly quickPickService: QuickPickService;
readonly onNoBoardsInstalled: () => void;
readonly onUnknownBoard: (board: Board) => void;
}
export interface State {
boardsLoading: boolean;
boards?: Board[];
otherBoard?: Board;
selection?: string;
}
export namespace Styles {
export const CONNECTED_BOARDS_CLASS = 'connected-boards';
}
}

View File

@@ -1,12 +0,0 @@
import { AbstractDialog } from "@theia/core/lib/browser";
export class InstallationProgressDialog extends AbstractDialog<string> {
readonly value: "does-not-matter";
constructor(componentName: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
}
}

View File

@@ -0,0 +1,23 @@
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string, version: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
}
}
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Uninstallation in progress' });
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
}
}

View File

@@ -0,0 +1,25 @@
import { injectable, inject, postConstruct } from 'inversify';
import { AboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoAboutDialog extends AboutDialog {
@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

@@ -0,0 +1,46 @@
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
import { injectable, inject } from 'inversify';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoApplicationShell extends ApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected refreshBottomPanelToggleButton() {
if (this.editorMode.proMode) {
super.refreshBottomPanelToggleButton();
}
}
protected async track(widget: Widget): Promise<void> {
if (this.editorMode.proMode) {
super.track(widget);
} else {
const tracker = (this as any).tracker as FocusTracker<Widget>;
tracker.add(widget);
this.disableClose(Saveable.apply(widget));
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const toTrack of await widget.getTrackableWidgets()) {
tracker.add(toTrack);
this.disableClose(Saveable.apply(toTrack));
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
}
}
}
}
private disableClose(widget: Widget | undefined): void {
if (widget instanceof EditorWidget) {
const onCloseRequest = (_: Message) => {
// NOOP
};
(widget as any).onCloseRequest = onCloseRequest.bind(widget);
}
}
}

View File

@@ -0,0 +1,23 @@
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { TextEditor } from '@theia/editor/lib/browser';
import { StatusBarAlignment } from '@theia/core/lib/browser';
export class ArduinoEditorContribution extends EditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {
}
protected setCursorPositionStatus(editor: TextEditor | undefined): void {
if (!editor) {
this.statusBar.removeElement('editor-status-cursor-position');
return;
}
const { cursor } = editor;
this.statusBar.setElement('editor-status-cursor-position', {
text: `${cursor.line + 1}`,
alignment: StatusBarAlignment.LEFT,
priority: 100
});
}
}

View File

@@ -0,0 +1,42 @@
import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { EditorMode } from '../editor-mode';
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
@injectable()
export class ArduinoFrontendApplication extends FrontendApplication {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected async initializeLayout(): Promise<void> {
return super.initializeLayout().then(() => {
// If not in PRO mode, we open the sketch file with all the related files.
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
if (!this.editorMode.proMode) {
this.workspaceService.roots.then(roots => {
for (const root of roots) {
this.fileSystem.exists(root.uri).then(exists => {
if (exists) {
this.frontendContribution.openSketchFiles(root.uri);
}
});
}
});
}
});
}
}

View File

@@ -0,0 +1,11 @@
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@@ -0,0 +1,18 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoNavigatorContribution extends FileNavigatorContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -0,0 +1,19 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -0,0 +1,18 @@
import { inject, injectable } from 'inversify';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
if (this.editorMode.proMode) {
super.registerToolbarItems(toolbarRegistry);
}
}
}

View File

@@ -0,0 +1,35 @@
import { inject, injectable } from 'inversify';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoProblemContribution extends ProblemContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
protected setStatusBarElement(problemStat: ProblemStat): void {
if (this.editorMode.proMode) {
super.setStatusBarElement(problemStat);
}
}
registerKeybindings(keybindings: KeybindingRegistry): void {
if (this.toggleCommand) {
keybindings.registerKeybinding({
command: this.toggleCommand.id,
keybinding: 'ctrlcmd+alt+shift+m'
});
}
}
}

View File

@@ -0,0 +1,24 @@
import { inject, injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoScmContribution extends ScmContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
if (this.editorMode.proMode) {
super.setStatusBarEntry(id, entry);
}
}
}

View File

@@ -0,0 +1,18 @@
import { inject, injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -1,26 +0,0 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
@injectable()
export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
}

View File

@@ -1,11 +0,0 @@
import { injectable } from 'inversify';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
@injectable()
export class SilentProblemContribution extends ProblemContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
}

View File

@@ -1,495 +1,115 @@
{
"tokenColors": [
{
"settings": {
"background": "#FFFFFF",
"foreground": "#000000"
}
},
{
"name": "Source",
"scope": "source",
"settings": {
"background": "#FFFFFF",
"fontStyle": ""
}
},
{
"name": "Comment",
"scope": "comment",
"settings": {
"fontStyle": "italic",
"foreground": "#91a6a6"
}
},
{
"name": "Docblock",
"scope": "comment.block",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Docblock tag",
"scope": "keyword.other.phpdoc",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Core function",
"scope": "support.function",
"settings": {
"fontStyle": "",
"foreground": "#6f8100"
}
},
{
"name": "Keyword",
"scope": [
"keyword",
"storage"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "Keyword operator",
"scope": "keyword.operator.assignment",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "keyword like new",
"scope": "keyword.other.special-method",
"settings": {
"fontStyle": ""
}
},
{
"name": "Function (definition)",
"scope": [
"entity.name.function",
"keyword.other.name-of-parameter.objc",
"support.type.exception.python"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "Class (definition)",
"scope": "entity.name",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "Number",
"scope": "constant.numeric",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "Variable",
"scope": [
"variable.language",
"variable.other"
],
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "Built-in constant",
"scope": "constant.language",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "Variable",
"scope": "variable.other.constant.ruby",
"settings": {
"foreground": "#0F8787"
}
},
{
"name": "Built-in constant",
"scope": [
"constant.language",
"support.function.construct"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "String",
"scope": "string",
"settings": {
"background": "#EB104512",
"foreground": "#006062"
}
},
{
"name": "Inherited class",
"scope": "entity.other.inherited-class",
"settings": {
"fontStyle": "",
"foreground": "#0F8787"
}
},
{
"name": "Inherited class seperator",
"scope": "punctuation.separator.inheritance",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "punctuation block",
"scope": "punctuation.separator.variable",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "array brackets",
"scope": "punctuation.section.array",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "hash separator",
"scope": "punctuation.separator.key-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "() brackets",
"scope": "punctuation.section.function",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Method call",
"scope": "meta.function-call",
"settings": {
"fontStyle": "",
"foreground": "#108888"
}
},
{
"name": "Method call",
"scope": "meta.function-call.python",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "hash brackets",
"scope": "punctuation.section.scope",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Special ruby method",
"scope": "keyword.other.special-method.ruby",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "symbol",
"scope": "constant.other.symbol",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Support class",
"scope": "support.class.ruby",
"settings": {
"fontStyle": "",
"foreground": "#008080"
}
},
{
"name": "String.regexp",
"scope": "string.regexp",
"settings": {
"foreground": "#009926"
}
},
{
"name": "String embedded source",
"scope": "string.quoted source",
"settings": {
"foreground": "#000000"
}
},
{
"name": "doctype punctation",
"scope": "meta.tag.sgml.html",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype declaration",
"scope": "entity.name.tag.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype string",
"scope": "string.quoted.double.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "html tag punctuation",
"scope": "punctuation.definition.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html tag punctuation",
"scope": "entity.name.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html attribute",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "html attribute punctation",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "erb tags",
"scope": "punctuation.section.embedded.ruby",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "ruby string interpolation",
"scope": "source.ruby.embedded.source punctuation.section.embedded.ruby",
"settings": {
"foreground": "#CF1040"
}
},
{
"name": "css brackets",
"scope": "punctuation.section.property-list.css",
"settings": {
"fontStyle": ""
}
},
{
"name": "css property",
"scope": "support.type.property-name.css",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "css property punctuation",
"scope": "punctuation.separator.key-value.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css property value",
"scope": [
"meta.property-value",
"constant.other.color"
],
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "css ending",
"scope": "punctuation.terminator.rule.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css font",
"scope": "support.constant.font-name",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css keyword",
"scope": "keyword.other.unit",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "pseudo class",
"scope": "entity.other.attribute-name.pseudo-class",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css constant property value",
"scope": "support.constant.property-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "css attribute class",
"scope": "entity.other.attribute-name.class",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "css attribute id",
"scope": "entity.other.attribute-name.id",
"settings": {
"fontStyle": "bold",
"foreground": "#990000"
}
},
{
"name": "diff header from",
"scope": "meta.diff.header.from-file",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff header to",
"scope": "meta.diff.header.to-file",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff inserted",
"scope": "markup.inserted.diff",
"settings": {
"fontStyle": ""
}
},
{
"name": "diff deleted",
"scope": "markup.deleted.diff",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Function Parameters",
"scope": "variable.parameter",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Library object",
"scope": [
"support.function",
"support.variable",
"support.constant",
"support.class",
"support.type"
],
"settings": {
"fontStyle": "bold",
"foreground": "#4183c4"
}
},
{
"name": "Function Meta Call Arguments",
"scope": "meta.function-call.arguments",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Function Call",
"scope": "meta.function-call",
"settings": {
"fontStyle": ""
}
}
],
"colors": {
"editor.background": "#FFFFFF",
"editorCursor.foreground": "#000000",
"editor.foreground": "#000000",
"editorWhitespace.foreground": "#BFBFBF",
"editor.lineHighlightBackground": "#00000010",
"editor.selectionBackground": "#ffcb00"
},
"name": "Arduino"
}
"tokenColors": [
{
"settings": {
"foreground": "#434f54"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#95a5a6cc"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#00979D"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c"
],
"settings": {
"foreground": "#D35400"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"foreground": "#434f54"
}
},
{
"name": "strings",
"scope": [
"string.quoted.double"
],
"settings": {
"foreground": "#005C5F"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#728E00"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
],
"colors": {
"list.highlightForeground": "#006468",
"list.activeSelectionBackground": "#006468",
"editor.background": "#ffffff",
"editorCursor.foreground": "#434f54",
"editor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00",
"focusBorder": "#4db7bb99",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#f7f9f9",
"button.background": "#4db7bb",
"titleBar.activeBackground": "#006468",
"titleBar.activeForeground": "#ffffff",
"terminal.background": "#000000",
"terminal.foreground": "#e0e0e0",
"dropdown.border": "#ececec",
"dropdown.background": "#ececec",
"activityBar.background": "#ececec",
"activityBar.foreground": "#616161",
"statusBar.background": "#006468",
"secondaryButton.background": "#b5c8c9",
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#00979d",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1"
},
"type": "light",
"name": "Arduino"
}

View File

@@ -0,0 +1,72 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { EditorWidget } from '@theia/editor/lib/browser';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { BoardsListWidget } from './boards/boards-list-widget';
import { LibraryListWidget } from './library/library-list-widget';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
readonly menuContentChanged = new Emitter<void>();
protected app: FrontendApplication;
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 ArduinoShellLayoutRestorer
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 {
const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY);
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
}
}
export namespace EditorMode {
export const PRO_MODE_KEY = 'arduino-advanced-mode';
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
}

View File

@@ -0,0 +1,34 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoMonacoEditorProvider extends MonacoEditorProvider {
@inject(ConfigService)
protected readonly configService: ConfigService;
protected dataDirUri: string | undefined;
protected async getModel(uri: URI, toDispose: DisposableCollection): Promise<MonacoEditorModel> {
// `createMonacoEditorOptions` is not `async` so we ask the `dataDirUri` here.
// https://github.com/eclipse-theia/theia/issues/6234
const { dataDirUri } = await this.configService.getConfiguration()
this.dataDirUri = dataDirUri;
return super.getModel(uri, toDispose);
}
protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions {
const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId);
options.model = model.textEditorModel;
options.readOnly = model.readOnly;
if (this.dataDirUri) {
options.readOnly = new URI(this.dataDirUri).isEqualOrParent(new URI(model.uri));
}
return options;
}
}

View File

@@ -0,0 +1,869 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="198px"
height="99px"
viewBox="0 0 198 99"
enable-background="new 0 0 198 99"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="buttons.svg"><metadata
id="metadata327"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs325" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1215"
inkscape:window-height="1000"
id="namedview323"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="4"
inkscape:cx="45.252385"
inkscape:cy="36.224987"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><g
id="g5"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="16.608999"
r="12"
id="circle7"
style="fill:#ffcc00" /><polyline
stroke-miterlimit="10"
points="21.453,12.745 15.788,20.571 11.775,16.658 "
id="polyline9"
style="fill:none;stroke:#655100;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g11"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="49.465"
r="12"
id="circle13"
style="fill:#ffffff" /><polyline
stroke-miterlimit="10"
points="21.453,45.601 15.788,53.427 11.775,49.514 "
id="polyline15"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g17"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="82.476997"
r="12"
id="circle19"
style="fill:#4db7bb" /><polyline
stroke-miterlimit="10"
points="21.453,78.612 15.788,86.438 11.775,82.525 "
id="polyline21"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g23"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="16.608999"
r="12"
id="circle25"
style="fill:#ffcc00" /><polygon
points="45.412,15.313 49.307,15.313 49.307,11.53 54.701,16.875 49.331,22.245 49.331,18.563 45.412,18.539 "
id="polygon27"
style="fill:#655100" /></g><g
id="g29"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="49.465"
r="12"
id="circle31"
style="fill:#ffffff" /><polygon
points="45.412,48.169 49.307,48.169 49.307,44.386 54.701,49.731 49.331,55.101 49.331,51.419 45.412,51.394 "
id="polygon33"
style="fill:#006468" /></g><g
id="g35"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="82.476997"
r="12"
id="circle37"
style="fill:#4db7bb" /><polygon
points="45.412,81.18 49.307,81.18 49.307,77.397 54.701,82.743 49.331,88.113 49.331,84.43 45.412,84.406 "
id="polygon39"
style="fill:#006468" /></g><g
id="g41"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="6.1090002"
width="21"
height="21"
id="rect43"
style="fill:#ffcc00" /><polygon
points="114.44,19.083 114.44,15.116 110.586,15.116 116.032,9.621 121.502,15.091 117.751,15.091 117.726,19.083 "
id="polygon45"
style="fill:#655100" /><rect
x="110.511"
y="22.193001"
width="1"
height="1"
id="rect47"
style="fill:#655100" /><rect
x="112.518"
y="22.193001"
width="1"
height="1"
id="rect49"
style="fill:#655100" /><rect
x="114.517"
y="22.193001"
width="1"
height="1"
id="rect51"
style="fill:#655100" /><rect
x="116.525"
y="22.193001"
width="1"
height="1"
id="rect53"
style="fill:#655100" /><rect
x="118.524"
y="22.193001"
width="1"
height="1"
id="rect55"
style="fill:#655100" /><rect
x="120.531"
y="22.193001"
width="1"
height="1"
id="rect57"
style="fill:#655100" /></g><g
id="g59"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="6.1090002"
width="21"
height="21"
id="rect61"
style="fill:#ffcc00" /><polygon
points="150.79,9.621 150.79,13.588 154.644,13.588 149.198,19.083 143.728,13.612 147.479,13.613 147.504,9.621 "
id="polygon63"
style="fill:#655100" /><rect
x="143.65199"
y="22.193001"
width="1"
height="1"
id="rect65"
style="fill:#655100" /><rect
x="145.66"
y="22.193001"
width="1"
height="1"
id="rect67"
style="fill:#655100" /><rect
x="147.659"
y="22.193001"
width="1"
height="1"
id="rect69"
style="fill:#655100" /><rect
x="149.666"
y="22.193001"
width="1"
height="1"
id="rect71"
style="fill:#655100" /><rect
x="151.666"
y="22.193001"
width="1"
height="1"
id="rect73"
style="fill:#655100" /><rect
x="153.673"
y="22.193001"
width="1"
height="1"
id="rect75"
style="fill:#655100" /></g><g
id="g77"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="38.965"
width="21"
height="21"
id="rect79"
style="fill:#ffffff" /><polygon
points="114.44,51.939 114.44,47.971 110.586,47.971 116.032,42.476 121.502,47.947 117.751,47.947 117.726,51.939 "
id="polygon81"
style="fill:#006468" /><rect
x="110.511"
y="55.049"
width="1"
height="1"
id="rect83"
style="fill:#006468" /><rect
x="112.518"
y="55.049"
width="1"
height="1"
id="rect85"
style="fill:#006468" /><rect
x="114.517"
y="55.049"
width="1"
height="1"
id="rect87"
style="fill:#006468" /><rect
x="116.525"
y="55.049"
width="1"
height="1"
id="rect89"
style="fill:#006468" /><rect
x="118.524"
y="55.049"
width="1"
height="1"
id="rect91"
style="fill:#006468" /><rect
x="120.531"
y="55.049"
width="1"
height="1"
id="rect93"
style="fill:#006468" /></g><g
id="g95"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="38.965"
width="21"
height="21"
id="rect97"
style="fill:#ffffff" /><polygon
points="150.79,42.476 150.79,46.444 154.644,46.444 149.198,51.939 143.728,46.468 147.479,46.468 147.504,42.477 "
id="polygon99"
style="fill:#006468" /><rect
x="143.65199"
y="55.049"
width="1"
height="1"
id="rect101"
style="fill:#006468" /><rect
x="145.66"
y="55.049"
width="1"
height="1"
id="rect103"
style="fill:#006468" /><rect
x="147.659"
y="55.049"
width="1"
height="1"
id="rect105"
style="fill:#006468" /><rect
x="149.666"
y="55.049"
width="1"
height="1"
id="rect107"
style="fill:#006468" /><rect
x="151.666"
y="55.049"
width="1"
height="1"
id="rect109"
style="fill:#006468" /><rect
x="153.673"
y="55.049"
width="1"
height="1"
id="rect111"
style="fill:#006468" /></g><g
id="g113"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="71.976997"
width="21"
height="21"
id="rect115"
style="fill:#4db7bb" /><polygon
points="114.44,84.95 114.44,80.983 110.586,80.983 116.032,75.488 121.502,80.959 117.751,80.958 117.726,84.95 "
id="polygon117"
style="fill:#006468" /><rect
x="110.511"
y="88.060997"
width="1"
height="1"
id="rect119"
style="fill:#006468" /><rect
x="112.518"
y="88.060997"
width="1"
height="1"
id="rect121"
style="fill:#006468" /><rect
x="114.517"
y="88.060997"
width="1"
height="1"
id="rect123"
style="fill:#006468" /><rect
x="116.525"
y="88.060997"
width="1"
height="1"
id="rect125"
style="fill:#006468" /><rect
x="118.524"
y="88.060997"
width="1"
height="1"
id="rect127"
style="fill:#006468" /><rect
x="120.531"
y="88.060997"
width="1"
height="1"
id="rect129"
style="fill:#006468" /></g><g
id="g131"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="71.976997"
width="21"
height="21"
id="rect133"
style="fill:#4db7bb" /><polygon
points="150.79,75.488 150.79,79.455 154.644,79.455 149.198,84.95 143.728,79.48 147.479,79.48 147.504,75.488 "
id="polygon135"
style="fill:#006468" /><rect
x="143.65199"
y="88.060997"
width="1"
height="1"
id="rect137"
style="fill:#006468" /><rect
x="145.66"
y="88.060997"
width="1"
height="1"
id="rect139"
style="fill:#006468" /><rect
x="147.659"
y="88.060997"
width="1"
height="1"
id="rect141"
style="fill:#006468" /><rect
x="149.666"
y="88.060997"
width="1"
height="1"
id="rect143"
style="fill:#006468" /><rect
x="151.666"
y="88.060997"
width="1"
height="1"
id="rect145"
style="fill:#006468" /><rect
x="153.673"
y="88.060997"
width="1"
height="1"
id="rect147"
style="fill:#006468" /></g><g
id="g149"
transform="translate(-0.40299988,0)"><path
d="M 87.445,22.097"
id="path151"
inkscape:connector-curvature="0"
style="fill:#655100" /><rect
x="72.403"
y="6.1090002"
width="21"
height="21"
id="rect153"
style="fill:#ffcc00" /><polygon
points="83.44,10.094 84.441,10.094 88.421,14.079 88.421,15.057 87.445,15.057 83.44,15.057 "
id="polygon155"
style="fill:#655100" /><polygon
points="78.404,11.093 78.404,22.097 87.445,22.097 87.445,14.87 88.421,14.87 88.421,23.134 77.399,23.134 77.399,10.094 83.562,10.094 83.568,11.093 "
id="polygon157"
style="fill:#655100" /><rect
x="79.399002"
y="12.111"
width="0.995"
height="0.99599999"
id="rect159"
style="fill:#655100" /><rect
x="81.394997"
y="12.111"
width="0.995"
height="0.99599999"
id="rect161"
style="fill:#655100" /><rect
x="79.399002"
y="14.103"
width="0.995"
height="0.99599999"
id="rect163"
style="fill:#655100" /><rect
x="81.394997"
y="14.103"
width="0.995"
height="0.99599999"
id="rect165"
style="fill:#655100" /><rect
x="79.399002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect167"
style="fill:#655100" /><rect
x="81.394997"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect169"
style="fill:#655100" /><rect
x="83.403"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect171"
style="fill:#655100" /><rect
x="85.400002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect173"
style="fill:#655100" /><rect
x="79.399002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect175"
style="fill:#655100" /><rect
x="81.394997"
y="18.118"
width="0.995"
height="0.99599999"
id="rect177"
style="fill:#655100" /><rect
x="79.399002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect179"
style="fill:#655100" /><rect
x="81.394997"
y="20.132"
width="0.995"
height="0.99599999"
id="rect181"
style="fill:#655100" /><rect
x="83.403"
y="18.118"
width="0.995"
height="0.99599999"
id="rect183"
style="fill:#655100" /><rect
x="85.400002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect185"
style="fill:#655100" /><rect
x="83.403"
y="20.132"
width="0.995"
height="0.99599999"
id="rect187"
style="fill:#655100" /><rect
x="85.400002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect189"
style="fill:#655100" /></g><g
id="g191"
transform="translate(-0.40299988,0)"><path
d="M 87.445,54.953"
id="path193"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="38.965"
width="21"
height="21"
id="rect195"
style="fill:#ffffff" /><polygon
points="83.44,42.95 84.441,42.95 88.421,46.935 88.421,47.912 87.445,47.912 83.44,47.912 "
id="polygon197"
style="fill:#006468" /><polygon
points="78.404,43.949 78.404,54.953 87.445,54.953 87.445,47.726 88.421,47.726 88.421,55.99 77.399,55.99 77.399,42.95 83.586,42.95 83.599,43.951 "
id="polygon199"
style="fill:#006468" /><rect
x="79.399002"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect201"
style="fill:#006468" /><rect
x="81.394997"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect203"
style="fill:#006468" /><rect
x="79.399002"
y="46.959"
width="0.995"
height="0.99599999"
id="rect205"
style="fill:#006468" /><rect
x="81.394997"
y="46.959"
width="0.995"
height="0.99599999"
id="rect207"
style="fill:#006468" /><rect
x="79.399002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect209"
style="fill:#006468" /><rect
x="81.394997"
y="48.972"
width="0.995"
height="0.99599999"
id="rect211"
style="fill:#006468" /><rect
x="83.403"
y="48.972"
width="0.995"
height="0.99599999"
id="rect213"
style="fill:#006468" /><rect
x="85.400002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect215"
style="fill:#006468" /><rect
x="79.399002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect217"
style="fill:#006468" /><rect
x="81.394997"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect219"
style="fill:#006468" /><rect
x="79.399002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect221"
style="fill:#006468" /><rect
x="81.394997"
y="52.987"
width="0.995"
height="0.99599999"
id="rect223"
style="fill:#006468" /><rect
x="83.403"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect225"
style="fill:#006468" /><rect
x="85.400002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect227"
style="fill:#006468" /><rect
x="83.403"
y="52.987"
width="0.995"
height="0.99599999"
id="rect229"
style="fill:#006468" /><rect
x="85.400002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect231"
style="fill:#006468" /></g><g
id="g233"
transform="translate(-0.40299988,0)"><path
d="M 87.445,87.965"
id="path235"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="71.976997"
width="21"
height="21"
id="rect237"
style="fill:#4db7bb" /><polygon
points="83.44,75.961 84.441,75.961 88.421,79.946 88.421,80.924 87.445,80.924 83.44,80.924 "
id="polygon239"
style="fill:#006468" /><polygon
points="78.404,76.961 78.404,87.965 87.445,87.965 87.445,80.738 88.421,80.738 88.421,89.002 77.399,89.002 77.399,75.961 83.586,75.961 83.599,76.963 "
id="polygon241"
style="fill:#006468" /><rect
x="79.399002"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect243"
style="fill:#006468" /><rect
x="81.394997"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect245"
style="fill:#006468" /><rect
x="79.399002"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect247"
style="fill:#006468" /><rect
x="81.394997"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect249"
style="fill:#006468" /><rect
x="79.399002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect251"
style="fill:#006468" /><rect
x="81.394997"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect253"
style="fill:#006468" /><rect
x="83.403"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect255"
style="fill:#006468" /><rect
x="85.400002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect257"
style="fill:#006468" /><rect
x="79.399002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect259"
style="fill:#006468" /><rect
x="81.394997"
y="83.986"
width="0.995"
height="0.99599999"
id="rect261"
style="fill:#006468" /><rect
x="79.399002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect263"
style="fill:#006468" /><rect
x="81.394997"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect265"
style="fill:#006468" /><rect
x="83.403"
y="83.986"
width="0.995"
height="0.99599999"
id="rect267"
style="fill:#006468" /><rect
x="85.400002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect269"
style="fill:#006468" /><rect
x="83.403"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect271"
style="fill:#006468" /><rect
x="85.400002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect273"
style="fill:#006468" /></g><g
id="g275"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="6.1090002"
width="21"
height="21"
id="rect277"
style="fill:#ffcc00" /><rect
x="187.819"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect279"
style="fill:#655100" /><rect
x="189.825"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect281"
style="fill:#655100" /><rect
x="174.83299"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect283"
style="fill:#655100" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="16.101"
r="4.0900002"
id="circle285"
style="fill:none;stroke:#655100;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="18.427999"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"
width="2.6800001"
height="4.4229999"
id="rect287"
style="fill:#655100" /><rect
x="180.789"
y="15.103"
width="2.0280001"
height="1.9960001"
id="rect289"
style="fill:#655100" /></g><g
id="g291"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="38.965"
width="21"
height="21"
id="rect293"
style="fill:#ffffff" /><rect
x="187.819"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect295"
style="fill:#006468" /><rect
x="189.825"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect297"
style="fill:#006468" /><rect
x="174.83299"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect299"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="48.957001"
r="4.0900002"
id="circle301"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="51.284"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,93.3163,-111.5246)"
width="2.6800001"
height="4.4229999"
id="rect303"
style="fill:#006468" /><rect
x="180.789"
y="47.959"
width="2.0280001"
height="1.9960001"
id="rect305"
style="fill:#006468" /></g><g
id="g307"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="71.976997"
width="21"
height="21"
id="rect309"
style="fill:#4db7bb" /><rect
x="187.819"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect311"
style="fill:#006468" /><rect
x="189.825"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect313"
style="fill:#006468" /><rect
x="174.83299"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect315"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="81.969002"
r="4.0900002"
id="circle317"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="84.295998"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,117.1625,-101.3412)"
width="2.6800001"
height="4.4229999"
id="rect319"
style="fill:#006468" /><rect
x="180.789"
y="80.970001"
width="2.0280001"
height="1.9960001"
id="rect321"
style="fill:#006468" /></g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 962 B

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" width="198px" height="99px" x="0px" y="0px" enable-background="new 0 0 198 99" version="1.1" viewBox="0 0 198 99" inkscape:version="0.91 r13725" sodipodi:docname="buttons.svg" xml:space="preserve">
<metadata id="metadata327">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>
image/svg+xml
</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs325"/>
<sodipodi:namedview id="namedview323" bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" objecttolerance="10" pagecolor="#ffffff" showgrid="false" showguides="true" inkscape:current-layer="Layer_1" inkscape:cx="45.252385" inkscape:cy="36.224987" inkscape:guide-bbox="true" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1000" inkscape:window-maximized="1" inkscape:window-width="1215" inkscape:window-x="65" inkscape:window-y="24" inkscape:zoom="4"/>
<g id="g5" transform="translate(-0.12000084,0)">
<polyline id="polyline9" stroke-miterlimit="10" points="21.453,12.745 15.788,20.571 11.775,16.658 " style="fill:none;stroke:#000000;stroke-width:2.30489993;stroke-miterlimit:10"/>
</g>
<g id="g23" transform="translate(-0.26100159,0)">
<polygon id="polygon27" points="45.412,15.313 49.307,15.313 49.307,11.53 54.701,16.875 49.331,22.245 49.331,18.563 45.412,18.539 " style="fill:#000000"/>
</g>
<g id="g41" transform="translate(-0.54399872,0)">
<polygon id="polygon45" points="114.44,19.083 114.44,15.116 110.586,15.116 116.032,9.621 121.502,15.091 117.751,15.091 117.726,19.083 " style="fill:#000000"/>
<rect id="rect47" width="1" height="1" x="110.511" y="22.193001" style="fill:#000000"/>
<rect id="rect49" width="1" height="1" x="112.518" y="22.193001" style="fill:#000000"/>
<rect id="rect51" width="1" height="1" x="114.517" y="22.193001" style="fill:#000000"/>
<rect id="rect53" width="1" height="1" x="116.525" y="22.193001" style="fill:#000000"/>
<rect id="rect55" width="1" height="1" x="118.524" y="22.193001" style="fill:#000000"/>
<rect id="rect57" width="1" height="1" x="120.531" y="22.193001" style="fill:#000000"/>
</g>
<g id="g59" transform="translate(-0.68600464,0)">
<polygon id="polygon63" points="150.79,9.621 150.79,13.588 154.644,13.588 149.198,19.083 143.728,13.612 147.479,13.613 147.504,9.621 " style="fill:#000000"/>
<rect id="rect65" width="1" height="1" x="143.65199" y="22.193001" style="fill:#000000"/>
<rect id="rect67" width="1" height="1" x="145.66" y="22.193001" style="fill:#000000"/>
<rect id="rect69" width="1" height="1" x="147.659" y="22.193001" style="fill:#000000"/>
<rect id="rect71" width="1" height="1" x="149.666" y="22.193001" style="fill:#000000"/>
<rect id="rect73" width="1" height="1" x="151.666" y="22.193001" style="fill:#000000"/>
<rect id="rect75" width="1" height="1" x="153.673" y="22.193001" style="fill:#000000"/>
</g>
<g id="g149" transform="translate(-0.40299988,0)">
<path id="path151" d="M 87.445,22.097" style="fill:#000000" inkscape:connector-curvature="0"/>
<polygon id="polygon155" points="83.44,10.094 84.441,10.094 88.421,14.079 88.421,15.057 87.445,15.057 83.44,15.057 " style="fill:#000000"/>
<polygon id="polygon157" points="78.404,11.093 78.404,22.097 87.445,22.097 87.445,14.87 88.421,14.87 88.421,23.134 77.399,23.134 77.399,10.094 83.562,10.094 83.568,11.093 " style="fill:#000000"/>
<rect id="rect159" width="0.995" height="0.99599999" x="79.399002" y="12.111" style="fill:#000000"/>
<rect id="rect161" width="0.995" height="0.99599999" x="81.394997" y="12.111" style="fill:#000000"/>
<rect id="rect163" width="0.995" height="0.99599999" x="79.399002" y="14.103" style="fill:#000000"/>
<rect id="rect165" width="0.995" height="0.99599999" x="81.394997" y="14.103" style="fill:#000000"/>
<rect id="rect167" width="0.995" height="0.99599999" x="79.399002" y="16.115999" style="fill:#000000"/>
<rect id="rect169" width="0.995" height="0.99599999" x="81.394997" y="16.115999" style="fill:#000000"/>
<rect id="rect171" width="0.995" height="0.99599999" x="83.403" y="16.115999" style="fill:#000000"/>
<rect id="rect173" width="0.995" height="0.99599999" x="85.400002" y="16.115999" style="fill:#000000"/>
<rect id="rect175" width="0.995" height="0.99599999" x="79.399002" y="18.118" style="fill:#000000"/>
<rect id="rect177" width="0.995" height="0.99599999" x="81.394997" y="18.118" style="fill:#000000"/>
<rect id="rect179" width="0.995" height="0.99599999" x="79.399002" y="20.132" style="fill:#000000"/>
<rect id="rect181" width="0.995" height="0.99599999" x="81.394997" y="20.132" style="fill:#000000"/>
<rect id="rect183" width="0.995" height="0.99599999" x="83.403" y="18.118" style="fill:#000000"/>
<rect id="rect185" width="0.995" height="0.99599999" x="85.400002" y="18.118" style="fill:#000000"/>
<rect id="rect187" width="0.995" height="0.99599999" x="83.403" y="20.132" style="fill:#000000"/>
<rect id="rect189" width="0.995" height="0.99599999" x="85.400002" y="20.132" style="fill:#000000"/>
</g>
<g id="g275" transform="translate(-0.82800293,0)">
<rect id="rect279" width="0.99900001" height="0.99800003" x="187.819" y="16.101" style="fill:#000000"/>
<rect id="rect281" width="0.99900001" height="0.99800003" x="189.825" y="16.101" style="fill:#000000"/>
<rect id="rect283" width="0.99900001" height="0.99800003" x="174.83299" y="16.101" style="fill:#000000"/>
<circle id="circle285" cx="181.80299" cy="16.101" r="4.0900002" stroke-miterlimit="10" style="fill:none;stroke:#000000;stroke-width:1.95439994;stroke-miterlimit:10"/>
<rect id="rect287" width="2.6800001" height="4.4229999" x="175.895" y="18.427999" style="fill:#000000" transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"/>
<rect id="rect289" width="2.0280001" height="1.9960001" x="180.789" y="15.103" style="fill:#000000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,53 @@
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';
import { Board, BoardPackage } from '../../common/protocol/boards-service';
@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));
const restartIfAffected = (pkg: BoardPackage) => {
if (!this.boardConfig) {
this.restart();
return;
}
const { selectedBoard } = this.boardConfig;
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
this.restart();
}
}
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
}
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

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../components/component-list/list-widget';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
super({
id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon',
searchable: service,
installable: service,
itemLabel: (item: Library) => item.name,
itemRenderer
});
}
}

View File

@@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class LibraryListWidget extends ListWidget {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
protected widgetProps(): ListWidget.Props {
return {
id: LibraryListWidget.WIDGET_ID,
title: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon'
}
}
}

View File

@@ -1,20 +1,12 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
await this.openView();
}
}
@injectable()
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution {
export class LibraryListWidgetFrontendContribution extends AbstractViewContribution<LibraryListWidget> implements FrontendApplicationContribution {
constructor() {
super({
@@ -22,11 +14,24 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
widgetName: LibraryListWidget.WIDGET_LABEL,
defaultWidgetOptions: {
area: 'left',
rank: 600
rank: 700
},
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'ctrlcmd+shift+l'
});
}
async initializeLayout(): Promise<void> {
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Manage Libraries...'
});
}
}
}

View File

@@ -1,72 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { LibraryService } from '../../common/protocol/library-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(WindowService)
protected readonly windowService: WindowService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
return <FilterableListContainer
service={this.libraryService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -0,0 +1,35 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Diagnostic } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core';
import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoProblemManager extends ProblemManager {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
@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}`));
}
setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(uri)) {
return [];
}
return super.setMarkers(uri, owner, data);
}
}

View File

@@ -0,0 +1,13 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser';
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
@injectable()
export class ArduinoMenuContribution extends BrowserMenuBarContribution {
onStart(app: FrontendApplication): void {
const menu = this.factory.createMenuBar();
app.shell.addWidget(menu, { area: 'top' });
}
}

View File

@@ -0,0 +1,10 @@
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { ArduinoMenuContribution } from './arduino-menu-contribution';
import { ContainerModule, interfaces } from 'inversify';
import '../../../src/browser/style/browser-menu.css'
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
unbind(BrowserMenuBarContribution);
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
});

View File

@@ -0,0 +1,265 @@
import { injectable, inject, postConstruct } from 'inversify';
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 { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
@injectable()
export class MonitorConnection {
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel;
@inject(MonitorService)
protected readonly monitorService: MonitorService;
@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected boardsServiceClient: BoardsServiceClientImpl;
@inject(MessageService)
protected messageService: MessageService;
@inject(FrontendApplicationStateService)
protected readonly applicationState: FrontendApplicationStateService;
protected state: MonitorConnection.State | undefined;
/**
* Note: The idea is to toggle this property from the UI (`Monitor` view)
* and the boards config and the boards attachment/detachment logic can be at on place, here.
*/
protected _autoConnect: boolean = false;
protected readonly onConnectionChangedEmitter = new Emitter<MonitorConnection.State | undefined>();
/**
* This emitter forwards all read events **iff** the connection is established.
*/
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
/**
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/
protected monitorErrors: MonitorError[] = [];
protected reconnectTimeout?: number;
@postConstruct()
protected init(): void {
// Forward the messages from the board **iff** connected.
this.monitorServiceClient.onRead(event => {
if (this.connected) {
this.onReadEmitter.fire(event);
}
});
this.monitorServiceClient.onError(async error => {
let shouldReconnect = false;
if (this.state) {
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug(`Connection was canceled by client: ${MonitorConnection.State.toString(this.state)}.`);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(`Connection failed. Serial port is busy: ${Port.toString(port)}.`, options);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(`Disconnected ${Board.toString(board, { useFqbn: false })} from ${Port.toString(port)}.`, options);
break;
}
case undefined: {
this.messageService.error(`Unexpected error. Reconnecting ${Board.toString(board)} on port ${Port.toString(port)}.`, options);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(`Failed to reconnect ${Board.toString(board, { useFqbn: false })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(port)} serial port is busy. after 10 consecutive attempts.`);
this.monitorErrors.length = 0;
} else {
const attempts = (this.monitorErrors.length || 1);
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(`Reconnecting ${Board.toString(board, { useFqbn: false })} to ${Port.toString(port)} in ${attempts} seconds...`, { timeout });
this.reconnectTimeout = window.setTimeout(() => this.connect(oldState.config), timeout);
}
}
}
});
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.boardsServiceClient.onBoardsChanged(event => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect()
.then(() => this.connect({ board, port, baudRate }));
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// 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;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
}
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
this.state = { config };
console.info(`<<< Serial monitor connection created for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
}
this.onConnectionChangedEmitter.fire(this.state);
return Status.isOK(connectStatus);
}
async disconnect(): Promise<Status> {
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
return Status.OK;
}
console.log('>>> Disposing existing monitor connection...');
const status = await this.monitorService.disconnect();
if (Status.isOK(status)) {
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
} else {
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
return status;
}
/**
* Sends the data to the connected serial monitor.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!this.connected) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>(resolve => {
this.monitorService.send(data + this.monitorModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<MonitorReadEvent> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise<void> {
if (this.autoConnect) {
if (this.boardsServiceClient.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 }) => {
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
new Promise<void>(resolve => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
}
}
}
}
export namespace MonitorConnection {
export interface State {
readonly config: MonitorConfig;
}
export namespace State {
export function toString(state: State): string {
const { config } = state;
const { board, port } = config;
return `${Board.toString(board)} ${Port.toString(port)}`;
}
}
}

View File

@@ -0,0 +1,119 @@
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';
@injectable()
export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onChangeEmitter: Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _baudRate: MonitorConfig.BaudRate;
protected _lineEnding: MonitorModel.EOL;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._baudRate = MonitorConfig.BaudRate.DEFAULT;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this.onChangeEmitter = new Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>();
}
onStart(): void {
this.localStorageService.getData<MonitorModel.State>(MonitorModel.STORAGE_ID).then(state => {
if (state) {
this.restoreState(state);
}
});
}
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event;
}
get autoscroll(): boolean {
return this._autoscroll;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.storeState();
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'autoscroll', value: this._autoscroll }));
}
get timestamp(): boolean {
return this._timestamp;
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'timestamp', value: this._timestamp }));
}
get baudRate(): MonitorConfig.BaudRate {
return this._baudRate;
}
set baudRate(baudRate: MonitorConfig.BaudRate) {
this._baudRate = baudRate;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'baudRate', value: this._baudRate }));
}
get lineEnding(): MonitorModel.EOL {
return this._lineEnding;
}
set lineEnding(lineEnding: MonitorModel.EOL) {
this._lineEnding = lineEnding;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'lineEnding', value: this._lineEnding }));
}
protected restoreState(state: MonitorModel.State): void {
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._baudRate = state.baudRate;
this._lineEnding = state.lineEnding;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this._baudRate,
lineEnding: this._lineEnding
});
}
}
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
baudRate: MonitorConfig.BaudRate;
lineEnding: EOL;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}
export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}

View File

@@ -0,0 +1,23 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
@injectable()
export class MonitorServiceClientImpl implements MonitorServiceClient {
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
protected readonly onErrorEmitter = new Emitter<MonitorError>();
readonly onRead = this.onReadEmitter.event;
readonly onError = this.onErrorEmitter.event;
notifyRead(event: MonitorReadEvent): void {
this.onReadEmitter.fire(event);
const { data } = event;
console.debug(`Received data: ${data}`);
}
notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error);
}
}

View File

@@ -0,0 +1,138 @@
import * as React from 'react';
import { injectable, inject } from "inversify";
import { AbstractViewContribution } from "@theia/core/lib/browser";
import { MonitorWidget } from "./monitor-widget";
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
import { ArduinoMenus } from "../arduino-frontend-contribution";
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { MonitorModel } from './monitor-model';
export namespace SerialMonitor {
export namespace Commands {
export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll',
label: 'Autoscroll'
}
export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp',
label: 'Timestamp'
}
export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output',
label: 'Clear Output',
iconClass: 'clear-all'
}
}
}
@injectable()
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR = MonitorWidget.ID + ':toggle-toolbar';
@inject(MonitorModel) protected readonly model: MonitorModel;
constructor() {
super({
widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor',
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'ctrlcmd+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output'
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: widget => widget instanceof MonitorWidget,
isVisible: widget => widget instanceof MonitorWidget,
execute: widget => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
}
}
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.openView({
toggle: true,
activate: true
})
});
const toolbarCmd = {
id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR
}
commands.registerCommand(toolbarCmd, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
execute: () => this.openView({
toggle: true,
activate: true
})
});
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div
title='Toggle Autoscroll'
className={`item enabled fa fa-angle-double-down arduino-monitor ${this.model.autoscroll ? 'toggled' : ''}`}
onClick={this.toggleAutoScroll}
></div>
</React.Fragment>;
}
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll(): Promise<void> {
this.model.toggleAutoscroll();
}
protected renderTimestampButton(): React.ReactNode {
return <React.Fragment key='line-ending-toolbar-item'>
<div
title='Toggle Timestamp'
className={`item enabled fa fa-clock-o arduino-monitor ${this.model.timestamp ? 'toggled' : ''}`}
onClick={this.toggleTimestamp}
></div>
</React.Fragment>;
}
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp(): Promise<void> {
this.model.toggleTimestamp();
}
}

View File

@@ -0,0 +1,333 @@
import * as React from 'react';
import * as dateFormat from 'dateformat';
import { postConstruct, injectable, inject } from 'inversify';
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 { 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';
import { ArduinoSelect } from '../components/arduino-select';
import { MonitorModel } from './monitor-model';
import { MonitorConnection } from './monitor-connection';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@injectable()
export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor';
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
protected widgetHeight: number;
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
/**
* Guard against re-rendering the view after the close was requested.
* See: https://github.com/eclipse-theia/theia/issues/6704
*/
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(this.monitorConnection.onConnectionChanged(() => this.clearConsole()));
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) {
this.monitorConnection.disconnect();
}
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
}
}
protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
requestAnimationFrame(() => MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest));
}
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'New Line',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
];
}
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
const baudRates: Array<MonitorConfig.BaudRate> = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate }));
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding = lineEndings.find(item => item.value === this.monitorModel.lineEnding) || lineEndings[1]; // Defaults to `\n`.
const baudRate = baudRates.find(item => item.value === this.monitorModel.baudRate) || baudRates[4]; // Defaults to `9600`.
return <div className='serial-monitor'>
<div className='head'>
<div className='send'>
<SerialMonitorSendInput
monitorConfig={this.monitorConnection.monitorConfig}
resolveFocus={this.onFocusResolved}
onSend={this.onSend} />
</div>
<div className='config'>
<div className='select'>
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
defaultValue={lineEnding}
onChange={this.onChangeLineEnding} />
</div>
<div className='select'>
<ArduinoSelect
className='select'
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate} />
</div>
</div>
</div>
<div className='body'>
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event} />
</div>
</div>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.monitorConnection.send(value);
}
protected readonly onChangeLineEnding = (option: SelectOption<MonitorModel.EOL>) => {
this.monitorModel.lineEnding = option.value;
}
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
this.monitorModel.baudRate = option.value;
}
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly monitorConfig?: MonitorConfig;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
export interface State {
text: string;
}
}
export class SerialMonitorSendInput extends React.Component<SerialMonitorSendInput.Props, SerialMonitorSendInput.State> {
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '' };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
render(): React.ReactNode {
return <input
ref={this.setRef}
type='text'
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
onKeyDown={this.onKeyDown} />
}
protected get placeholder(): string {
const { monitorConfig } = this.props;
if (!monitorConfig) {
return 'Not connected. Select a board and a port to connect automatically.'
}
const { board, port } = monitorConfig;
return `Message (${isOSX ? '⌘' : 'Ctrl'}+Enter to send message to '${Board.toString(board, { useFqbn: false })}' on '${Port.toString(port)}')`;
}
protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
}
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
}
protected onSend(): void {
this.props.onSend(this.state.text);
this.setState({ text: '' });
}
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend();
}
}
}
}
export namespace SerialMonitorOutput {
export interface Props {
readonly monitorModel: MonitorModel;
readonly monitorConnection: MonitorConnection;
readonly clearConsoleEvent: Event<void>;
}
export interface State {
content: string;
timestamp: boolean;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props, SerialMonitorOutput.State> {
/**
* Do not touch it. It is used to be able to "follow" the serial monitor log.
*/
protected anchor: HTMLElement | null;
protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props);
this.state = { content: '', timestamp: this.props.monitorModel.timestamp };
}
render(): React.ReactNode {
return <React.Fragment>
<div style={({ whiteSpace: 'pre', fontFamily: 'monospace' })}>
{this.state.content}
</div>
<div style={{ float: 'left', clear: 'both' }} ref={element => { this.anchor = element; }} />
</React.Fragment>;
}
componentDidMount(): void {
this.scrollToBottom();
let chunk = '';
this.toDisposeBeforeUnmount.pushAll([
this.props.monitorConnection.onRead(({ data }) => {
chunk += data;
const eolIndex = chunk.indexOf('\n');
if (eolIndex !== -1) {
const line = chunk.substring(0, eolIndex + 1);
chunk = chunk.slice(eolIndex + 1);
const content = `${this.state.content}${this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`;
this.setState({ content });
}
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
})
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
}
}
}
export interface SelectOption<T> {
readonly label: string;
readonly value: T;
}

View File

@@ -0,0 +1,24 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
@injectable()
export class ArduinoShellLayoutRestorer extends ShellLayoutRestorer {
// Workaround for https://github.com/eclipse-theia/theia/issues/6579.
async storeLayoutAsync(app: FrontendApplication): Promise<void> {
if (this.shouldStoreLayout) {
try {
this.logger.info('>>> Storing the layout...');
const layoutData = app.shell.getLayoutData();
const serializedLayoutData = this.deflate(layoutData);
await this.storageService.setData(this.storageKey, serializedLayoutData);
this.logger.info('<<< The layout has been successfully stored.');
} catch (error) {
await this.storageService.setData(this.storageKey, undefined);
this.logger.error('Error during serialization of layout data', error);
}
}
}
}

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