Compare commits

..

176 Commits

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

Closes arduino/arduino-pro-ide#311

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

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

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

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

after:
arduino-pro-ide_0.0.7-snapshot.1b8c510_Linux_64bit.zip
2020-08-24 05:12:10 -07:00
Akos Kitta
d5c7d3a8ed Merge pull request #140 from bcmi-labs/0.1.0-rc
0.1.0 rc
2020-08-24 12:45:24 +02:00
Akos Kitta
0ddfa1cfdc Use the 0.12.0 CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-24 11:36:07 +02:00
Akos Kitta
d13a43668e Made the port optional for upload using programmer
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-24 10:27:36 +02:00
Akos Kitta
5f515f0597 fixup: made the Output view not closeable.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-21 17:15:28 +02:00
Akos Kitta
1078f6fa96 Updated the version from 0.0.7 to 0.1.0.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-21 11:51:03 +02:00
Akos Kitta
5ac7fe1446 Use 0.12.0-rc3 CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-21 11:50:22 +02:00
Akos Kitta
0363041b39 Merge pull request #130 from bcmi-labs/cli-0.11.0-rc1-62-g72c9655f
0.0.7 RC build
2020-08-20 16:26:13 +02:00
Akos Kitta
4c4043b73c Updated to 0.12.0-rc2 CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-20 14:45:32 +02:00
Akos Kitta
fc0fe2196f Merge pull request #138 from bcmi-labs/ci--increase-workflow-timeout
Increased the timeout to 90 minutes
2020-08-19 12:50:20 +02:00
Akos Kitta
82a3b01c0b increased the timeout to 90.
notarization can run more than expected, we rely on Apple services.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 12:14:19 +02:00
Akos Kitta
7754cf6334 increased the timeout to 90.
notarization can run more than expected, we rely on Apple services.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 11:30:06 +02:00
Akos Kitta
cb1c16fb26 made the linter happy: removed unused imports
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:52 +02:00
Akos Kitta
d07763a854 Updated to 0.12.0-rc1 CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:52 +02:00
Akos Kitta
f605994d7d fixed duplicate preferences menu item.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:52 +02:00
Akos Kitta
e72e794266 aligned the bindings for windwos.
quit and close.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:52 +02:00
Akos Kitta
ee327eff18 patched to fix the window reloading on windows
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:52 +02:00
Akos Kitta
960d8dba2e removed that uses a more recent version of the CLI from the PATH
from now on, we always use the one from the IDE. Documented how to replace it on demand.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
17de5e1a35 ATL-312: Fixed output resize issue.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
af21f07f84 set the default tab size to 2 spaces.
closes ATL-379.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
2e729fc29e workaround for theia-ide/theia#7828
instead otf reloaading the current window, we open a new one
and close the current.

reloading the current window hangs on electron in Windows env.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
bc4c3e04f5 fixed issue when checking if a sketch is temp
convert all windows drive letters to lower case. [ATL-380]

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
aa2bed8d39 fixed sketch opening. This was the happy path.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
3806d56e94 queued the menu updates.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
f0015e3fb4 ignored out failing port is in use tests.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
650230a571 refined open logic when sketch name is invalid
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
aadd403cdb refined label provide for unsaved sketches
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
6da702abd5 styled the sketch-control
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
4cad068e8d styled the sketch control.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
bb705620c0 made the editor focusing a bit more relaxed at startup.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
e36e42910c fixed delete -> cancel
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
a49ba05d5e wipe the entire sketch when deleting the main file
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
b87782ed05 re-enabled decorators in output.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
e4e6217153 finished the output styling.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
bcc715e11c [theme]: made the output stylable.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
7f0bdfd45e check if we need output adjustments.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
efd1be3e85 wipe original after sketch rename.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
eb2161cc6e changed the default ref behavior.
instead of opening the widget next to current, open it after the last.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
9ee5fc097c fixed closeable state.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
f375202c5d set focus on main sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
fc09dc3613 aligned api visibility with theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
3d55aaa875 generalized which widgets are closeable.
instead of a whitelisting, we blacklist the files those belong to sketch

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
fc866464ad removed unused code.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
ada0f4c7ed aligned rename with the java IDE.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
111ba7fef3 allow multiple dots.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
1f7e06f990 aligned new file creation to the java ide.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
f503ef0fcf [revert]: skip the failing daemon test on win32.
it's with the latest CLI. Some error message must have changed.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
43abbf6fb8 removed the YAML extension from the app
See: arduino/arduino-pro-ide#293
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
5aae800a4e when switching serial monitor on/off
open/dispose the monitor widget instead of toggling it.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
a2b3d9b314 zen mode for the output
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
06c3015158 updated to 1.14.0 theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
8ab70f48f8 fixed save-as. added sketchload
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
528f4150d3 ATL-318: Fixed monitor reconnection after upload
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
696048b5d9 do not reveal the output for daemon messages
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
722f8d7534 programmer was removed from compilereqSigned-off-by: Akos Kitta <kittaakos@typefox.io> 2020-08-19 10:50:51 +02:00
Akos Kitta
4a9a975dca ATL-301: Made port optional when using programmer
When uploading using a programmer, the port is ignored by the CLI.
Also removed `programmer` from compile request [arduino/arduino-cli#861]

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
6661f661c7 Updated to 0.11.0-rc1-62-g72c9655f CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
a54c860dbb dispose the tool ouput when the window closes.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-08-19 10:50:51 +02:00
Akos Kitta
84016f4e1e Merge pull request #136 from bcmi-labs/per1234/sign-in-gh-hosted-runner
Use GitHub hosted macOS GitHub Actions runner instead of self hosted runner
2020-08-19 10:47:46 +02:00
per1234
fd2b03e286 Use GitHub hosted macOS GitHub Actions runner instead of self hosted runner
The self hosted runner was used because the electron-builder signing process was hanging when the workflow was run on the GitHub hosted macOS runner. Now that the electron-builder signing certificate import procedure can be used, the GitHub hosted runner can be used without any problems so there is no longer any need for a self hosted runner.
2020-08-17 09:22:39 -07:00
per1234
ce58590c0e Use electron-builder to import macOS signing certificate
The previous certificate import system causes electron-builder's code signing process to hang when the GitHub hosted macOS runner is used.

electron-builder has a built-in system for importing the code signing certificate into the keychain. This requires the certificate to have been exported using a specific procedure, which is described vaguely in the electron-builder documentation:

https://www.electron.build/code-signing

And more thoroughly in this article:

https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate

The "KEYCHAIN" secret that was previously in use was apparently produced by some other procedure. The use of that secret results in a "security: SecKeychainItemImport: Unknown format in import." error during the electron-builder's certificate import process. The APPLE_SIGNING_CERTIFICATE_P12 secret was generated by following the instructions in the article at the link above and works fine with the electron-builder certificate import system.
2020-08-17 09:22:39 -07:00
Akos Kitta
1b8c510f33 Merge pull request #128 from bcmi-labs/external-programmer
[ATL-119]: Rework the menu organization for IDE 2.0, [ATL-75]: External programmer
2020-07-23 16:13:18 +02:00
Akos Kitta
5d8fbcd437 patched the electron-maindropped -psn arg on macOS.Signed-off-by: Akos Kitta <kittaakos@typefox.io> 2020-07-23 08:55:56 +02:00
Akos Kitta
747f5d6a14 fixed the save-as contribution.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 16:08:13 +02:00
Akos Kitta
d9f4adfb78 fixed menu updater.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 14:32:35 +02:00
Akos Kitta
d51bf9fb40 warn when uploading with programmer but not set
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 13:54:17 +02:00
Akos Kitta
f4097b5b24 upload with programmer when it's explicitly asked
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 13:16:43 +02:00
Akos Kitta
491e0cb6d0 fixed the current editor issue.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 12:06:33 +02:00
Akos Kitta
e95f00466f disabled linter errors.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 11:54:20 +02:00
Akos Kitta
e1f74d94a7 fixed hoisting. updated to next Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 11:54:04 +02:00
Akos Kitta
01c21266e5 filter out no match.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 11:45:32 +02:00
Akos Kitta
8aa08cbf6e fixed fuzzy. added proper boost.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-22 11:33:28 +02:00
Akos Kitta
fdc5814e66 make sure the sketch file has the focus after the ws open.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 20:44:37 +02:00
Akos Kitta
87b383f57e use programmer id for upload/verify
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 20:12:51 +02:00
Akos Kitta
cc76f2bbc8 clear the output before upload/verify
use the same channel for stdout and and stderr

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 19:52:59 +02:00
Akos Kitta
deea43008d toast compile/verify
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 19:40:02 +02:00
Akos Kitta
42d2285fa8 style outline.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 19:39:49 +02:00
Akos Kitta
8b27cd7bb9 switch to the corresponding channel on message.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 19:34:43 +02:00
Akos Kitta
230bacfd01 fix sketch uri issue.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 18:49:23 +02:00
Akos Kitta
e1d86d0bda fixed the programmer menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 18:20:19 +02:00
Akos Kitta
e77c9721cd init: programmers
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 17:39:22 +02:00
Akos Kitta
07692fe368 removed unused injection.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 12:46:39 +02:00
Akos Kitta
3465407b5a fixed too early disposal when hiding context menu
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 12:46:02 +02:00
Akos Kitta
89c348baac styled the sketch control contribution.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 11:41:57 +02:00
Akos Kitta
824d1df4bd aligned the title with the Java IDE.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 10:54:31 +02:00
Akos Kitta
445ffedf02 patched the keybinding registry
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-21 09:31:45 +02:00
Akos Kitta
bbf880d187 redesigned the settings menu group.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 22:02:54 +02:00
Akos Kitta
12a13b869c added sketch control.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 19:28:27 +02:00
Akos Kitta
271b06d98a Added quit app for windows and linux
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 17:20:38 +02:00
Akos Kitta
587eb87416 added quit app.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 17:09:23 +02:00
Akos Kitta
2f8ac44152 fixed the issue with the terminal.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 16:59:06 +02:00
Akos Kitta
47fb5105c5 removed unused styles.
for the output we use a monaco editor.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 16:44:38 +02:00
Akos Kitta
673c61506e GH-262: Added workaround for duplicate board names
Non-unique board names per core caused an issue in the boards config.

Closes: arduino/arduino-pro-ide#262
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 15:40:58 +02:00
Akos Kitta
79a5d2272f fixed grpc client issue with the CLI config.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 15:38:49 +02:00
Akos Kitta
537291b494 Pulled in @theia/preferences, removed @theia/cpp
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 14:24:17 +02:00
Akos Kitta
23450ce863 removed auto-save.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 13:58:47 +02:00
Akos Kitta
c9dfc68911 no validation when appending core settings to fqbn
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 12:11:20 +02:00
Akos Kitta
0ee9d16b40 fuzzy board search.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 11:29:05 +02:00
Akos Kitta
2f3fe27da3 Port: is.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 10:30:21 +02:00
Akos Kitta
dfed2350bd pimped the status bar: boards+port. Fixed HC theme
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-20 10:28:50 +02:00
Akos Kitta
a74b159366 renamed customization folder to theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 21:09:08 +02:00
Akos Kitta
5788ca3761 aligned items under tools.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 21:02:46 +02:00
Akos Kitta
5d23cb5270 Implemented font size mod.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 20:57:15 +02:00
Akos Kitta
95dfd3920c aligned tools and sketch menu. added auto-format
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 18:08:24 +02:00
Akos Kitta
89faa9d45c Open in external.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 18:00:26 +02:00
Akos Kitta
42d5a08c3b cloned the editor menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 17:30:54 +02:00
Akos Kitta
88631a318b aligned track to current theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 15:54:08 +02:00
Akos Kitta
540a6215e4 implemented a few more edit contributions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 15:43:55 +02:00
Akos Kitta
c0cadf2b9b comment/uncomment line
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 14:57:25 +02:00
Akos Kitta
939a905a6b go to line.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 14:43:46 +02:00
Akos Kitta
33af9b6005 implemented copy to forum.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 14:22:54 +02:00
Akos Kitta
e0d4634f32 cleaned up save group.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 13:59:52 +02:00
Akos Kitta
83e074e895 aligned module names.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 13:35:50 +02:00
Akos Kitta
7873e492d4 fixed close sketch
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 13:34:36 +02:00
Akos Kitta
e8c3abd2ec Resrtuctured browser code.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-18 12:51:07 +02:00
Akos Kitta
3a6b2f2bc8 More refactoring. Splitting up code.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 18:04:38 +02:00
Akos Kitta
cfde197198 Use CLI 0.11.0.
- LS does not work,
 - Cannot reconnect to the daemon on browser refresh.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 16:23:45 +02:00
Akos Kitta
20f44fe072 more refactoring.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 16:10:02 +02:00
Akos Kitta
5abfc2c1aa chopped up the FE contribution
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 15:00:49 +02:00
Akos Kitta
b6df6b57a1 Aligned the IDE with gRPC server startup message.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 10:21:40 +02:00
Akos Kitta
218aaf64c7 updated to latest Theia, CLI, electron and node 12
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-17 09:41:02 +02:00
Akos Kitta
a0bbfe0b5c init
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-16 16:33:49 +02:00
Akos Kitta
4326c5f9f2 Merge pull request #124 from bcmi-labs/build-on-arm
Documented how to build from sources on ARM Linux.
2020-07-16 14:16:24 +02:00
Akos Kitta
d29141aff6 Made the CLI config closeable in classic mode.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:30:14 +02:00
Akos Kitta
55ce04fe3f From now on the user can toggle the bottom panel.
It was disabled in classic mode.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:29:40 +02:00
Akos Kitta
2eab9a0792 Do not automatically reveal the Output view.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:28:37 +02:00
Akos Kitta
88dcaba3ae From now on, the Output toolbar is visible in classic mode.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:27:52 +02:00
Akos Kitta
f392ce752d Make sure the files from the sketch folder are opened at startup.
Previously, we opened the files only in classic mode.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:26:56 +02:00
Akos Kitta
2bd9eef146 Aligned the New and Save As... with the Java IDE.
From now on, sketches are created in the temp folder,
and will be moved to the `directories.user` location
when the user performs a manual `Save`.

A new sketch can be created with the `CtrlCmd+N` binding.

Closes: arduino/arduino-pro-ide#260
Closes: arduino/arduino-pro-ide#261

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 16:26:42 +02:00
Akos Kitta
20bd7bcd64 Documented how to build from sources on ARM Linux.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-15 10:44:41 +02:00
Akos Kitta
3082b3d643 Merge pull request #111 from bcmi-labs/debugger_cli_update
[debugger] Added --interpreter mi2 flag
2020-07-15 10:24:13 +02:00
Akos Kitta
18ca84d682 Merge pull request #120 from bcmi-labs/grpc-js
Switched from `grpc` to `@grpc/grpc-js`.
2020-07-13 17:28:02 +02:00
Akos Kitta
16cb2cf88b ATL-240: Fixed the VS Code extensions in the app.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-13 15:06:43 +02:00
Miro Spönemann
e7a134a181 [debugger] Added --interpreter mi2 flag 2020-07-13 14:47:26 +02:00
Akos Kitta
0942ef6450 Switched from grpc to @grpc/grpc-js.
This change is required to be able to support ARM arch
in the future.

 - Fixed the download links for armv7l Linux.
 - Checked in the generated code. [grpc/grpc-node#1497]
 - Made the code generation a manual step.
 - Made the `grpc-tools` dependency optional.
 - From now on, the serial port regex is case-insensitive.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-13 13:30:06 +02:00
Akos Kitta
68f95ae5b4 Merge pull request #123 from bcmi-labs/arch-doc
Documented basic architecture overview.
2020-07-09 20:21:43 +02:00
Akos Kitta
976f858c7c Documented basic architecture overview.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-09 16:10:31 +02:00
Akos Kitta
ee196bfc3a Merge pull request #121 from bcmi-labs/fix-build-badge
Fixed the build-badge in the README.
2020-07-09 12:37:41 +02:00
Akos Kitta
8e8a8ac945 Fixed the expected tag prefix.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-09 10:23:02 +02:00
Akos Kitta
2f93af4faa Fixed the build-badge in the README.
- Added the missing link to GH Actions.
 - Fixed  a typo.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-09 09:17:15 +02:00
Akos Kitta
66d29d6155 Merge pull request #119 from bcmi-labs/doc-ci-cd
Updated the documentation.
2020-07-08 14:47:37 +02:00
Akos Kitta
45f06718b4 Updated the documentation.
- [macOS] Removed `zip` target. We need it later for the auto-update.
 - [CI]: From now on, the release tag must not start with `v`.
 - [yarn]: Added steps on how to rebuild the natives from the source.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-08 14:21:06 +02:00
Akos Kitta
08e3731493 Merge pull request #118 from bcmi-labs/release-workflow-fixes
Fixed the version part in the artifact name.
2020-07-07 17:21:59 +02:00
Akos Kitta
bf1351ffdc Fixed the version part in the artifact name.
Removed unused `require`s.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-07 17:02:05 +02:00
Akos Kitta
8da8f2a50e Merge pull request #117 from bcmi-labs/gh_actions_needs
Made the upload and release depend on the build job.
2020-07-07 15:37:27 +02:00
Akos Kitta
5a353ffaa5 Made the upload and release depend on the build job.
- Split up the pipeline to build, publish, and release.
 - Set a 30 minutes build timeout, to avoid macOS signing issues.
 - Removed global envs. `env` cannot be accessed for `if` in jobs.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-07 15:17:01 +02:00
Akos Kitta
951884045c Merge pull request #114 from bcmi-labs/gh-actions
Gh actions
2020-07-03 15:01:49 +02:00
Akos Kitta
6bb852c613 Build on GH Actions. Set up nightly.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-07-03 14:26:17 +02:00
Akos Kitta
d9a866579c Include the VS Code C++ extension in the app.
Closes arduino/arduino-pro-ide#257

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-06-29 13:58:58 +02:00
Akos Kitta
ecce237c6c Updated to Theia 1.4.0-next.a62011a5.
- Aligned the daemon test to latest `js-yaml`.
 - Aligned the TS version with Theia.
 - Resolved a dependency-cycle with `next` Theia.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-06-29 13:58:32 +02:00
Akos Kitta
ae6aec27db Updated the CLI to 0.11.0.
- Adjusted to gRPC API change, and
 - Log the CLI commit only once when generating from protoc.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-06-29 13:19:43 +02:00
Akos Kitta
f6f6cb20de Merge pull request #113 from bcmi-labs/0.0.7
Updated to `0.0.7` after the release.
2020-05-07 09:53:01 +02:00
Akos Kitta
02d1f349ad Updated to 0.0.7 after the release.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-05-07 09:20:34 +02:00
Akos Kitta
f71a266fe3 Merge pull request #112 from bcmi-labs/GH-240
GH-240: Warn if not connected to the Internet
2020-04-27 15:38:06 +02:00
Akos Kitta
d8a742e440 GH-240: Warn if not connected to the Internet.
Make sure the gRPC client initialization is rejected.

Closes #240.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-27 14:41:57 +02:00
Akos Kitta
f1fca4d5b3 Updated to 1.1.0-next.bda9ff9d Theia.
Also bumped up the `lerna` dependency as it mysteriously made the
`nsfw` module re-build incorrectly for electron and caused a runtime
error due to missing `lodash.isnumber` dependency.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-27 14:37:36 +02:00
Akos Kitta
4732a1d68b Merge pull request #110 from bcmi-labs/ci
0.0.6-alpha RC
2020-04-27 10:43:28 +02:00
Akos Kitta
6ce4143d49 [ci]: Made various changes for the electron app:
- Support for multiple electron targe per platform.
 - Removed packager CLI. Changed the logic we calculate the app name.
 - Fixed various OS-specific tests: stubbed `os`.
 - Restructured the final ZIP formats for Windows and Linux.
 - Added packager tests.
 - Switched from `@grpc/grpc-js` to native `grpc`.
 - Updated the version from 0.0.5 to 0.0.6.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-09 08:55:43 +02:00
Akos Kitta
d54a69935e [experimental]: Introduced the Boards Control.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-09 08:55:43 +02:00
Akos Kitta
12f2aa35ff Added support for 3rd party core settings.
Closes arduino/arduino-pro-ide#10.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-09 08:55:43 +02:00
Akos Kitta
5c16f8d6c9 From now on, monitor widget does not expect EOL.
Otherwise, if client code does not contain `Serial.write('\n')`,
the widget does not show any output.

Closes arduino/arduino-pro-ide#201

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-09 08:55:43 +02:00
Akos Kitta
807b2ad424 Support of the CLI config.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-04-09 08:55:38 +02:00
Akos Kitta
c7bf98dfa3 Merge pull request #106 from bcmi-labs/issue192
Fixed Open... dialog
2020-03-27 11:18:07 +01:00
Miro Spönemann
b2d53b1a35 #192: Fixed Open... dialog 2020-03-26 09:39:18 +01:00
Akos Kitta
a9bbf47a59 Merge pull request #107 from bcmi-labs/arduino/arduino-pro-ide#212
Wait when opening all files from a sketch folder.
2020-03-17 16:33:12 +01:00
Akos Kitta
3974f2d8a1 Merge pull request #105 from sbhklr/new-icons
Add new app icons
2020-03-12 21:03:06 +01:00
Akos Kitta
2a2238bb82 Wait when opening all files from a sketch folder.
This commit is a temporary workaround for eclipse-theia/theia#6298.

Closes arduino/arduino-pro-ide#212.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-03-12 14:57:38 +01:00
Sebastian Hunkeler
44504ab916 Add new app icons 2020-03-05 00:33:08 +01:00
224 changed files with 21894 additions and 8168 deletions

133
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,133 @@
name: Arduino Pro IDE
on:
push:
branches:
- master
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
pull_request:
branches:
- master
schedule:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
jobs:
build:
strategy:
matrix:
config:
- os: windows-2016
- os: ubuntu-latest
- os: macos-latest
# - os: rsora-rpi-arm # self-hosted armhf
runs-on: ${{ matrix.config.os }}
timeout-minutes: 90
env:
CERTIFICATE_PATH: /tmp/macos_signing_certificate.p12
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js 10.x
uses: actions/setup-node@v1
with:
node-version: '10.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Python 2.7
uses: actions/setup-python@v2
with:
python-version: '2.7'
- name: Generate signing certificate file [macOS]
if: runner.OS == 'macOS'
run: |
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"
- name: Package
shell: bash
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
# electron-builder will try to sign during the Windows job if these environment variables are defined
if [ "${{ runner.OS }}" = "macOS" ]; then
# See: https://www.electron.build/code-signing
export CSC_LINK="${{ env.CERTIFICATE_PATH }}"
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
fi
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v2
with:
name: build-artifacts
path: electron/build/dist/build-artifacts/
publish:
needs: build
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
with:
name: build-artifacts
path: build-artifacts
- name: Publish Nightly [S3]
uses: kittaakos/upload-s3-action@v0.0.1
with:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_bucket: ${{ secrets.DOWNLOADS_BUCKET }}
source_dir: build-artifacts/
destination_dir: arduino-pro-ide/nightly/
release:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
with:
name: build-artifacts
path: build-artifacts
- name: Create Release [GitHub]
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@v1-release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build-artifacts/*
tag: ${{ github.ref }}
file_glob: true
- name: Publish Release [S3]
uses: kittaakos/upload-s3-action@v0.0.1
with:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_bucket: ${{ secrets.DOWNLOADS_BUCKET }}
source_dir: build-artifacts/
destination_dir: arduino-pro-ide/

10
.gitignore vendored
View File

@@ -8,10 +8,10 @@ build/
src-gen/
*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
yarn*.log
# For the VS Code extensions used by Theia.
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config

30
.vscode/launch.json vendored
View File

@@ -13,7 +13,7 @@
{
"type": "node",
"request": "launch",
"name": "Launch Electron Packager",
"name": "Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
},
@@ -36,7 +36,8 @@
"--hostname=localhost",
"--no-cluster",
"--remote-debugging-port=9222",
"--no-app-auto-install"
"--no-app-auto-install",
"--plugins=local-dir:plugins"
],
"env": {
"NODE_ENV": "development"
@@ -61,7 +62,8 @@
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install"
"--no-app-auto-install",
"--plugins=local-dir:plugins"
],
"windows": {
"env": {
@@ -85,24 +87,20 @@
{
"type": "node",
"request": "launch",
"name": "App (Browser - Debug CLI daemon)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"protocol": "inspector",
"name": "Run Test [current]",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--debug-cli=true"
"--require",
"reflect-metadata/Reflect",
"--no-timeouts",
"--colors",
"**/${fileBasenameNoExtension}.js"
],
"env": {
"NODE_ENV": "development"
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
},
"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"

126
.vscode/tasks.json vendored
View File

@@ -1,48 +1,80 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Arduino Editor - Start Browser Example",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": true
}
},
{
"label": "Arduino Editor - Watch Theia Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Editor - Watch Browser Example",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Editor - Watch All",
"type": "shell",
"dependsOn": [
"Arduino Editor - Watch Theia Extension",
"Arduino Editor - Watch Browser Example"
]
}
]
}
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Arduino Pro IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": true
}
},
{
"label": "Arduino Pro IDE - Watch IDE Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Pro IDE - Watch Debugger Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-debugger-extension watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Pro IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Pro IDE - Watch Electron App",
"type": "shell",
"command": "yarn --cwd ./electron-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino Pro IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Debugger Extension",
"Arduino Pro IDE - Watch Browser App"
]
},
{
"label": "Arduino Pro IDE - Watch All [Electron]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Debugger Extension",
"Arduino Pro IDE - Watch Electron App"
]
}
]
}

143
README.md
View File

@@ -1,50 +1,129 @@
# 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)
[![Arduino Pro IDE](https://github.com/bcmi-labs/arduino-editor/workflows/Arduino%20Pro%20IDE/badge.svg)](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22)
> **Beware:** This is very much work-in-progress. Things can and probably will be broken, even on master.
### Download
This repo contains a proof-of-concept for an Arduino IDE based on Theia.
It's built on top of a [fork of the arduino-cli](https://github.com/typefox/arduino-cli/tree/daemon) that sports a gRPC interface.
You can download the latest version of the Arduino Pro IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-pro-ide/releases) or following the links in the following table.
## How to try (offline)
#### Latest version
### 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).
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------ |
Linux | | [Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Windows 64 bit] |
macOS | | [macOS 64 bit] |
```
git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor
[🚧 Work in progress...]: https://github.com/arduino/arduino-pro-ide/issues/287
[Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Linux_64bit.zip
[Windows 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Windows_64bit.zip
[macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_macOS_64bit.dmg
#### Previous versions
These are available from the [GitHub releases page](https://github.com/arduino/arduino-pro-ide/releases).
#### Nightly builds
These builds are generated every day at 03:00 GMT from the `master` branch and
should be considered unstable. In order to get the latest nightly build
available for the supported platform, use the following links:
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------ |
Linux | | [Nightly Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Nightly Windows 64 bit] |
macOS | | [Nightly macOS 64 bit] |
[🚧 Work in progress...]: https://github.com/arduino/arduino-pro-ide/issues/287
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, 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` )
### Build from source
If youre familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
project, you should be able to build the Arduino Pro IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
### Build
```sh
yarn
```
### Rebuild the native dependencies
```sh
yarn rebuild:electron
yarn --cwd electron-app start
```
If you want to switch back to the browser-based example, execute the following in the repository root
```
yarn rebuild:browser
```
Then you can start the browser example again:
```
yarn --cwd browser-app start
### Start
```sh
yarn start
```
Click [here](./arduino-ide-extension/README.md) for more details on various IDE services, and the Arduino Pro IDE implementation in general.
### CI
This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22).
## Arduino Pro IDE Electron Application
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
- _Snapshot_ builds run when changes are pushed to the `master` branch, or when a PR is created against the `master` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
- _Nightly_ builds run every day at 03:00 GMT from the `master` branch.
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
- Create a local tag:
```sh
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
```
- Push it to the remote:
```sh
git push origin 1.2.3
```
Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows,
- macOS, and
- Linux.
You can download the latest release applications fom [here](https://github.com/bcmi-labs/arduino-editor/releases/latest).
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.
### FAQ
<img width="500" src="static/download_01.gif">
<img width="500" src="static/download_02.gif">
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
- A: Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.
- Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?
- A: [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
- Windows: `C:\path\to\Arduino Pro IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino Pro IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino Pro IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
### Architecture overview
The Pro IDE consists of three major parts:
- the _Electron main_ process,
- the _backend_, and
- the _frontend_.
The _Electron main_ process is responsible for:
- creating the application,
- managing the application lifecycle via listeners, and
- creating and managing the web pages for the app.
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process.
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the Pro IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the Pro IDE, only the _backend_ allows OS interaction.
The _backend_ process is responsible for:
- providing access to the filesystem,
- communicating with the Arduino CLI via gRPC,
- running your terminal,
- exposing additional RESTful APIs,
- performing the Git commands in the local repositories,
- hosting and running any VS Code extensions, or
- executing VS Code tasks<sup>[[2]]</sup>.
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
[2]: https://code.visualstudio.com/Docs/editor/tasks

View File

@@ -1,14 +1,11 @@
{
"name": "arduino-debugger-extension",
"version": "0.0.5",
"version": "0.1.1",
"description": "An extension for debugging Arduino programs",
"license": "MIT",
"engines": {
"node": ">=10.10.0"
},
"dependencies": {
"@theia/debug": "next",
"arduino-ide-extension": "0.0.5",
"arduino-ide-extension": "0.1.1",
"cdt-gdb-adapter": "^0.0.14",
"vscode-debugadapter": "^1.26.0",
"vscode-debugprotocol": "^1.26.0"
@@ -20,21 +17,14 @@
"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"
"src"
],
"theiaExtensions": [
{
"backend": "lib/node/backend-module",
"frontend": "lib/browser/frontend-module"
"backend": "lib/node/arduino-debug-backend-module",
"frontend": "lib/browser/arduino-debug-frontend-module"
}
]
}

View File

@@ -114,7 +114,7 @@ export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendAp
id: ArduinoDebugCommands.START_DEBUG.id,
command: ArduinoDebugCommands.START_DEBUG.id,
tooltip: 'Start Debugging',
priority: 1
priority: 3
});
}

View File

@@ -10,7 +10,6 @@ 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);

View File

@@ -3,7 +3,7 @@ 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';
import { ArduinoDaemonImpl } from 'arduino-ide-extension/lib/node/arduino-daemon-impl';
@injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
@@ -12,7 +12,7 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
readonly label = 'Arduino';
readonly languages = ['c', 'cpp', 'ino'];
@inject(ArduinoCli) arduinoCli: ArduinoCli;
@inject(ArduinoDaemonImpl) daemon: ArduinoDaemonImpl;
getSchemaAttributes(): IJSONSchema[] {
return [
@@ -66,7 +66,7 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
const startFunction = config.pauseAtMain ? 'main' : 'setup';
const res: ActualDebugConfig = {
...config,
arduinoCli: await this.arduinoCli.getExecPath(),
arduinoCli: await this.daemon.getExecPath(),
fqbn: '${fqbn}',
uploadPort: '${port}',
initCommands: [

View File

@@ -27,6 +27,7 @@ export class ArduinoGDBBackend extends GDBBackend {
'debug',
'-p', requestArgs.uploadPort || 'none',
'-b', requestArgs.fqbn,
'--interpreter', 'mi2',
sketchDir
];
const proc = spawn(command, args);

View File

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

View File

@@ -1,44 +1,8 @@
{
"name": "arduino-ide-extension",
"version": "0.0.5",
"version": "0.1.1",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"engines": {
"node": ">=10.10.0"
},
"dependencies": {
"@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",
"@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": {
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"clean": "rimraf lib",
@@ -48,17 +12,57 @@
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w",
"test": "mocha \"./test/**/*.test.ts\""
"test": "mocha \"./lib/test/**/*.test.js\"",
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"mocha": {
"require": [
"ts-node/register",
"reflect-metadata/Reflect"
],
"reporter": "spec",
"colors": true,
"watch-extensions": "ts,tsx",
"timeout": 10000
"dependencies": {
"@grpc/grpc-js": "^1.1.1",
"@theia/application-package": "next",
"@theia/core": "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/preferences": "next",
"@theia/output": "next",
"@theia/search-in-workspace": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@types/dateformat": "^3.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^5.0.35",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"deepmerge": "^4.2.2",
"fuzzy": "^0.1.3",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"lodash.debounce": "^4.0.8",
"js-yaml": "^3.13.1",
"ncp": "^2.0.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",
"temp": "^0.9.1",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1"
},
"devDependencies": {
"@types/chai": "^4.2.7",
@@ -70,19 +74,27 @@
"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",
"grpc_tools_node_protoc_ts": "^4.1.0",
"mocha": "^7.0.0",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"ts-node": "^8.6.2",
"tslint": "^5.5.0",
"typescript": "3.5.3",
"sinon": "^9.0.1",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
"optionalDependencies": {
"grpc-tools": "^1.9.0"
},
"mocha": {
"require": [
"reflect-metadata/Reflect"
],
"reporter": "spec",
"colors": true,
"watch-extensions": "js",
"timeout": 10000
},
"files": [
"lib",
"src",
@@ -91,12 +103,15 @@
],
"theiaExtensions": [
{
"backend": "lib/node/arduino-backend-module",
"frontend": "lib/browser/arduino-frontend-module"
"backend": "lib/node/arduino-ide-backend-module",
"frontend": "lib/browser/arduino-ide-frontend-module"
},
{
"frontend": "lib/browser/menu/browser-arduino-menu-module",
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontend": "lib/browser/boards/quick-open/boards-quick-open-module"
}
]
}

View File

@@ -10,7 +10,7 @@
(() => {
const DEFAULT_VERSION = '0.9.0'; // require('moment')().format('YYYYMMDD');
const DEFAULT_VERSION = '0.12.1'; // require('moment')().format('YYYYMMDD');
const path = require('path');
const shell = require('shelljs');
@@ -42,6 +42,7 @@
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm': return 'Linux_ARMv7.tar.gz';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;

View File

@@ -2,30 +2,15 @@
(async () => {
const DEFAULT_VERSION = 'nightly'; // '0.3.7-alpha.preview';
const os = require('os');
const path = require('path');
const glob = require('glob');
const { v4 } = require('uuid');
const shell = require('shelljs');
const protoc = path.dirname(require('protoc/protoc'));
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${protoc}`;
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
const 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);
@@ -34,48 +19,63 @@
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) {
const { platform } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const rawVersion = shell.exec(`${cli} version`).trim();
if (!rawVersion) {
shell.echo(`Could not retrieve the CLI version from ${cli}.`);
shell.exit(1);
}
const version = rawVersion.substring(rawVersion.lastIndexOf('Commit:') + 'Commit:'.length).trim();
if (version) {
if (shell.exec(`git -C ${repository} checkout ${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
}
shell.echo('Generating TS/JS API from:');
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');
shell.mkdir('-p', out);
const protos = await new Promise(resolve =>
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
if (error) {
shell.echo(error.stack);
resolve([]);
return;
}
resolve(matches.map(filename => path.join(rpc, filename)));
}));
if (!protos || protos.length === 0) {
shell.echo(`Could not find any .proto files under ${rpc}.`);
shell.exit(1);
}
// 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} \
--grpc_out=generate_package_definition:${out} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
${protos.join(' ')}`).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} \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`)} \
--ts_out=generate_package_definition:${out} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
${protos.join(' ')}`).code !== 0) {
shell.exit(1);
}
const { patch } = require('./patch-grpc-js');
patch([out])
shell.echo('Done.');
shell.echo('<<< Generation was successful.');
})();
})();

View File

@@ -1,38 +0,0 @@
// Use `@grpc/grpc-js` instead of `grpc` at runtime.
// https://github.com/grpc/grpc-node/issues/624
// 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.`);
}
}
}

View File

@@ -1,58 +1,30 @@
import { Command } from '@theia/core/lib/common/command';
/**
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
*/
export namespace ArduinoCommands {
export const VERIFY: Command = {
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"
}
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",
label: 'New Sketch',
category: 'File'
}
/**
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
*/
export const OPEN_SKETCH_FILES: Command = {
id: 'arduino-open-sketch-files'
};
export const OPEN_BOARDS_DIALOG: Command = {
id: "arduino-open-boards-dialog"
}
id: 'arduino-open-boards-dialog'
};
export const TOGGLE_ADVANCED_MODE: Command = {
id: "arduino-toggle-advanced-mode"
}
id: 'arduino-toggle-advanced-mode'
};
export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = {
id: "arduino-toggle-advanced-mode-toolbar"
}
id: 'arduino-toggle-advanced-mode-toolbar'
};
}

View File

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

View File

@@ -1,65 +1,50 @@
import * as React from 'react';
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, 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 { 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 { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core';
import {
ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
FrontendApplication, KeybindingContribution, KeybindingRegistry
ContextMenuRenderer,
FrontendApplication, FrontendApplicationContribution,
OpenerService, StatusBar, StatusBarAlignment
} 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 { 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'];
}
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu, EditorManager } from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem } from '@theia/filesystem/lib/common';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-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 { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, BoardsServiceClient, CoreService, Port, SketchesService, ToolOutputServiceClient } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsDataStore } from './boards/boards-data-store';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { EditorMode } from './editor-mode';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
@injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution, ColorContribution {
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@@ -70,14 +55,15 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(WorkspaceServiceExt)
protected readonly workspaceServiceExt: WorkspaceServiceExt;
@inject(ToolOutputServiceClient)
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
// Unused but do not remove it. It's required by DI, otherwise `init` method is not called.
@inject(BoardsServiceClient)
protected readonly boardsServiceClient: BoardsServiceClient;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@@ -109,8 +95,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@@ -136,33 +122,49 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected application: FrontendApplication;
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@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) => {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
}
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: BoardsConfig.Config.toString(config)
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
className: 'arduino-selected-board'
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
className: 'arduino-selected-port'
});
}
}
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));
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
}
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,
@@ -171,184 +173,53 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
this.problemContribution,
this.scmContribution,
this.siwContribution] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(this.application);
viewContribution.initializeLayout(app);
}
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: ArduinoCommands.VERIFY.id,
command: ArduinoCommands.VERIFY_TOOLBAR.id,
tooltip: 'Verify'
});
registry.registerItem({
id: ArduinoCommands.UPLOAD.id,
command: ArduinoCommands.UPLOAD_TOOLBAR.id,
tooltip: 'Upload'
});
registry.registerItem({
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} />,
boardsServiceClient={this.boardsServiceClientImpl} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 2
priority: 7
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Toggle Serial Monitor'
tooltip: '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)')
tooltip: this.editorMode.proMode ? 'Switch to Classic Mode' : 'Switch to 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, {
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();
},
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
execute: this.upload.bind(this)
});
registry.registerCommand(ArduinoCommands.UPLOAD_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: this.upload.bind(this)
});
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_SKETCH_FILES, {
execute: async (uri: string) => {
this.openSketchFiles(uri);
}
});
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 {
// hack: sometimes we don't get the workspace root, but the currently active file: correct for that
if (uri.path.ext !== "") {
uri = uri.withPath(uri.path.dir.dir);
}
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.OPEN_BOARDS_DIALOG, {
isEnabled: () => true,
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.boardsConfig = boardsConfig;
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
}
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
isToggled: () => this.editorMode.proMode,
execute: () => this.editorMode.toggleProMode()
@@ -360,256 +231,51 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
});
}
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);
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
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.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
}
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
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
});
})
})
}
async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
for (const uri of uris) {
this.editorManager.open(new URI(uri));
protected async openSketchFiles(uri: string): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri);
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
for (const uri of [mainFileUri, ...otherSketchFileUris, ...additionalFileUris]) {
await this.ensureOpened(uri);
}
});
await this.ensureOpened(mainFileUri, true);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
/**
* 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);
}
}
protected async ensureOpened(uri: string, forceOpen: boolean = false): Promise<any> {
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri));
}
return undefined;
}
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 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 {
if (arg instanceof URI) {
return arg;
}
if (typeof arg === 'string') {
return new URI(arg);
}
if (arg instanceof EditorWidget) {
return arg.editor.uri;
}
return undefined;
}
registerColors(colors: ColorRegistry): void {
@@ -638,6 +304,42 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
hc: 'editorWidget.background'
},
description: 'Color of the Arduino Pro IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground'
},
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.hoverBackground',
hc: 'activityBar.inactiveForeground'
},
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
},
{
id: 'arduino.output.foreground',
defaults: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground'
},
description: 'Color of the text in the Output view.'
},
{
id: 'arduino.output.background',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background'
},
description: 'Background color of the Output view.'
}
);
}

View File

@@ -1,244 +0,0 @@
import '../../src/browser/style/index.css';
import { ContainerModule, interfaces } from 'inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { 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';
import { WorkspaceServiceExt, WorkspaceServiceExtPath } from './workspace-service-ext';
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 { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/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(KeybindingContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
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);
bind(WidgetFactory).toDynamicValue(context => ({
id: LibraryListWidget.WIDGET_ID,
createWidget: () => context.container.get(LibraryListWidget)
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
// 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 => {
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();
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: BoardsListWidget.WIDGET_ID,
createWidget: () => context.container.get(BoardsListWidget)
}));
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))
.inSingletonScope();
// Tool output service client
bind(ToolOutputServiceClientImpl).toSelf().inSingletonScope();
bind(ToolOutputServiceClient).toDynamicValue(context => {
const client = context.container.get(ToolOutputServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ToolOutputService.SERVICE_PATH, client);
return client;
}).inSingletonScope();
// The workspace service extension
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;
});
// 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();
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
// 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

@@ -0,0 +1,361 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from 'inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution, TabBarToolbarFactory } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution, FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutlineViewContribution } from './theia/outline/outline-contribution';
import { ProblemContribution as TheiaProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ProblemContribution } from './theia/markers/problem-contribution';
import { FileNavigatorContribution } from './theia/navigator/navigator-contribution';
import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import { ApplicationShell } from './theia/core/application-shell';
import { FrontendApplication } from './theia/core/frontend-application';
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-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, ConfigServiceClient } 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 { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
import { ProblemManager } from './theia/markers/problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { AboutDialog } from './theia/core/about-dialog';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { ArduinoDaemonClientImpl } from './arduino-daemon-client-impl';
import { ArduinoDaemonClient, ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser';
import { EditorManager } from './theia/editor/editor-manager';
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution
} from '@theia/core/lib/browser/connection-status-service';
import { ConfigServiceClientImpl } from './config-service-client-impl';
import { CoreServiceClientImpl } from './core-service-client-impl';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
import {
WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution,
FileMenuContribution as TheiaFileMenuContribution,
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution
} from '@theia/workspace/lib/browser';
import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './theia/workspace/workspace-frontend-contribution';
import { Contribution } from './contributions/contribution';
import { NewSketch } from './contributions/new-sketch';
import { OpenSketch } from './contributions/open-sketch';
import { CloseSketch } from './contributions/close-sketch';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
import { OpenSketchExternal } from './contributions/open-sketch-external';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preferences-contribution';
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { KeybindingRegistry } from './theia/core/keybindings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget';
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, unbind, isBound, 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(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
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);
bind(WidgetFactory).toDynamicValue(context => ({
id: LibraryListWidget.WIDGET_ID,
createWidget: () => context.container.get(LibraryListWidget)
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(ConfigServiceClientImpl);
return connection.createProxy(ConfigServicePath, client);
}).inSingletonScope();
bind(ConfigServiceClientImpl).toSelf().inSingletonScope();
bind(ConfigServiceClient).toDynamicValue(context => {
const client = context.container.get(ConfigServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath, client);
return client;
}).inSingletonScope();
// Boards service
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(async context => {
const client = context.container.get(BoardsServiceClientImpl);
const service = context.container.get<BoardsService>(BoardsService);
await client.init(service);
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution).to(BoardsDataMenuUpdater).inSingletonScope();
bind(BoardsDataStore).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsDataStore);
// Logger for the Arduino daemon
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('store');
}).inSingletonScope().whenTargetNamed('store');
// Boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget
bind(BoardsListWidget).toSelf();
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: BoardsListWidget.WIDGET_ID,
createWidget: () => context.container.get(BoardsListWidget)
}));
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 => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(CoreServiceClientImpl);
return connection.createProxy(CoreServicePath, client);
}).inSingletonScope();
// Core service client to receive and delegate notifications when the index or the library index has been updated.
bind(CoreServiceClientImpl).toSelf().inSingletonScope();
bind(CoreServiceClient).toDynamicValue(context => {
const client = context.container.get(CoreServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, CoreServicePath, client);
return client;
}).inSingletonScope();
// Tool output service client
bind(ToolOutputServiceClientImpl).toSelf().inSingletonScope();
bind(ToolOutputServiceClient).toDynamicValue(context => {
const client = context.container.get(ToolOutputServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ToolOutputService.SERVICE_PATH, client);
return client;
}).inSingletonScope();
// 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 serial 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();
// Serial 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();
bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution).to(OutlineViewContribution).inSingletonScope();
rebind(TheiaProblemContribution).to(ProblemContribution).inSingletonScope();
rebind(TheiaFileNavigatorContribution).to(FileNavigatorContribution).inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaMonacoStatusBarContribution).to(MonacoStatusBarContribution).inSingletonScope();
rebind(TheiaApplicationShell).to(ApplicationShell).inSingletonScope();
rebind(TheiaScmContribution).to(ScmContribution).inSingletonScope();
rebind(TheiaSearchInWorkspaceFrontendContribution).to(SearchInWorkspaceFrontendContribution).inSingletonScope();
rebind(TheiaFrontendApplication).to(FrontendApplication).inSingletonScope();
rebind(TheiaWorkspaceFrontendContribution).to(WorkspaceFrontendContribution).inSingletonScope();
rebind(TheiaFileMenuContribution).to(ArduinoFileMenuContribution).inSingletonScope();
rebind(TheiaCommonFrontendContribution).to(CommonFrontendContribution).inSingletonScope();
rebind(TheiaPreferencesContribution).to(PreferencesContribution).inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution).to(WorkspaceCommandContribution).inSingletonScope();
rebind(TheiaWorkspaceDeleteHandler).to(WorkspaceDeleteHandler).inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(({ container: parentContainer }) => () => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
});
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
rebind(TheiaApplicationConnectionStatusContribution).toService(ApplicationConnectionStatusContribution);
bind(FrontendConnectionStatusService).toSelf().inSingletonScope();
rebind(TheiaFrontendConnectionStatusService).toService(FrontendConnectionStatusService);
// Editor customizations. Sets the editor to `readOnly` if under the data dir.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).toService(EditorManager);
// Decorator customizations
bind(TabBarDecoratorService).toSelf().inSingletonScope();
rebind(TheiaTabBarDecoratorService).toService(TabBarDecoratorService);
// Problem markers
bind(ProblemManager).toSelf().inSingletonScope();
rebind(TheiaProblemManager).toService(ProblemManager);
// About dialog to show the CLI version
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
// Arduino daemon client. Receives notifications from the backend if the CLI daemon process has been restarted.
bind(ArduinoDaemon).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(ArduinoDaemonClientImpl);
return connection.createProxy(ArduinoDaemonPath, client);
}).inSingletonScope();
bind(ArduinoDaemonClientImpl).toSelf().inSingletonScope();
bind(ArduinoDaemonClient).toDynamicValue(context => {
const client = context.container.get(ArduinoDaemonClientImpl);
WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath, client);
return client;
}).inSingletonScope();
// File-system extension
bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope();
Contribution.configure(bind, NewSketch);
Contribution.configure(bind, OpenSketch);
Contribution.configure(bind, CloseSketch);
Contribution.configure(bind, SaveSketch);
Contribution.configure(bind, SaveAsSketch);
Contribution.configure(bind, VerifySketch);
Contribution.configure(bind, UploadSketch);
Contribution.configure(bind, OpenSketchExternal);
Contribution.configure(bind, EditContributions);
Contribution.configure(bind, QuitApp);
Contribution.configure(bind, SketchControl);
Contribution.configure(bind, Settings);
});

View File

@@ -9,7 +9,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
* `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?
* - `hash` points to an existing location?
* - Yes
* - `validate location`. Is valid sketch location?
* - Yes

View File

@@ -1,79 +0,0 @@
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';
@injectable()
export class ArduinoWorkspaceService extends WorkspaceService {
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@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();
}
})();
return this.workspaceUri;
}
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

@@ -4,10 +4,9 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
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 { InstallationProgressDialog } from '../widgets/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.
@@ -35,9 +34,9 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
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)))
this.boardsService.search({}).then(packages => {
const candidates = packages
.filter(pkg => pkg.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length

View File

@@ -5,6 +5,8 @@ import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CoreServiceClientImpl } from '../core-service-client-impl';
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@@ -15,6 +17,12 @@ export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(CoreServiceClientImpl)
protected readonly coreServiceClient: CoreServiceClientImpl;
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
@@ -38,6 +46,8 @@ export class BoardsConfigDialogWidget extends ReactWidget {
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
coreServiceClient={this.coreServiceClient}
daemonClient={this.daemonClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
@@ -51,5 +61,4 @@ export class BoardsConfigDialogWidget extends ReactWidget {
(this.focusNode || this.node).focus();
}
}

View File

@@ -1,7 +1,9 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsService, Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CoreServiceClientImpl } from '../core-service-client-impl';
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
export namespace BoardsConfig {
@@ -13,6 +15,8 @@ export namespace BoardsConfig {
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly coreServiceClient: CoreServiceClientImpl;
readonly daemonClient: ArduinoDaemonClientImpl;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
}
@@ -21,6 +25,7 @@ export namespace BoardsConfig {
searchResults: Array<Board & { packageName: string }>;
knownPorts: Port[];
showAllPorts: boolean;
query: string;
}
}
@@ -31,11 +36,11 @@ export abstract class Item<T> extends React.Component<{
selected: boolean,
onClick: (item: T) => void,
missing?: boolean,
detail?: string
details?: string
}> {
render(): React.ReactNode {
const { selected, label, missing, detail } = this.props;
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
@@ -43,11 +48,11 @@ export abstract class Item<T> extends React.Component<{
if (missing === true) {
classNames.push('missing')
}
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!details ? '' : details}`}>
<div className='label'>
{label}
</div>
{!detail ? '' : <div className='detail'>{detail}</div>}
{!details ? '' : <div className='details'>{details}</div>}
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
</div>;
}
@@ -70,19 +75,25 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
searchResults: [],
knownPorts: [],
showAllPorts: false,
query: '',
...boardsConfig
}
}
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
const { boardsServiceClient: client } = this.props;
this.props.boardsService.getAvailablePorts().then(ports => this.updatePorts(ports));
const { boardsServiceClient, coreServiceClient, daemonClient } = this.props;
this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
boardsServiceClient.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
boardsServiceClient.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
})
}),
boardsServiceClient.onBoardsPackageInstalled(() => this.updateBoards(this.state.query)),
boardsServiceClient.onBoardsPackageUninstalled(() => this.updateBoards(this.state.query)),
coreServiceClient.onIndexUpdated(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStarted(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStopped(() => this.setState({ searchResults: [] }))
]);
}
@@ -100,11 +111,12 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
? eventOrQuery
: eventOrQuery.target.value.toLowerCase()
).trim();
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
this.setState({ query });
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
}
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
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))) {
@@ -114,35 +126,17 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
});
}
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 queryBoards = (options: { query?: string } = {}): Promise<Array<Board & { packageName: string }>> => {
return this.props.boardsServiceClient.searchBoards(options);
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected get availablePorts(): Promise<{ ports: Port[] }> {
protected get availablePorts(): Promise<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 queryPorts = async (availablePorts: Promise<Port[]> = this.availablePorts) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
}
protected toggleFilterPorts = () => {
@@ -184,25 +178,15 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
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;
}
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) => !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
return false;
}
return <React.Fragment>
@@ -211,14 +195,14 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
{Array.from(distinctBoards.values()).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)}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={!Board.installed(board)}
missing={board.missing}
/>)}
</div>
</React.Fragment>;
@@ -266,9 +250,9 @@ export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.is(other)) {
if (Board.is(other)) {
return !!selectedBoard
&& Board.equals(other, selectedBoard)
&& Port.sameAs(selectedPort, other.port);

View File

@@ -0,0 +1,117 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { Board, ConfigOption, Programmer } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
}
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update();
if (selectedBoard) {
const { fqbn } = selectedBoard;
if (fqbn) {
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
if (configOptions.length) {
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z01_boardsConfig']; // `z_` is for ordering.
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
const menuPath = [...boardsConfigMenuPath, `${option}`];
const commands = new Map<string, Disposable & { label: string }>()
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id };
const selectedValue = value.value;
const handler = {
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
isToggled: () => value.selected
};
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
}
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
...Array.from(commands.keys()).map((commandId, i) => {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
})
]);
}
}
if (programmers.length) {
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(Disposable.create(() => this.unregisterSubmenu(programmersMenuPath)));
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
const handler = {
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
};
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
this.commandRegistry.registerCommand(command, handler);
this.toDisposeOnBoardChange.pushAll([
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
]);
}
}
this.mainMenuManager.update();
}
}
});
}
protected unregisterSubmenu(menuPath: string[]): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = this.menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
}

View File

@@ -0,0 +1,200 @@
import { injectable, inject, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol';
@injectable()
export class BoardsDataStore implements FrontendApplicationContribution {
@inject(ILogger)
@named('store')
protected readonly logger: ILogger;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => {
const { installedVersion: version } = pkg;
if (!version) {
return;
}
let shouldFireChanged = false;
for (const fqbn of pkg.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
shouldFireChanged = true;
}
}
}
}
if (shouldFireChanged) {
this.fireChanged();
}
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string> {
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(
fqbn: string,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
const version = await boardsPackageVersion;
if (!version) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
if (data) {
if (data.programmers !== undefined) { // to be backward compatible. We did not save the `programmers` into the `localStorage`.
return data;
}
}
const boardDetails = await this.getBoardDetailsSafe(fqbn);
if (!boardDetails) {
return BoardsDataStore.Data.EMPTY;
}
data = { configOptions: boardDetails.configOptions, programmers: boardDetails.programmers };
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{ fqbn, selectedProgrammer }: { fqbn: string, selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { programmers } = data;
if (!programmers.find(p => Programmer.equals(selectedProgrammer, p))) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data: { ...data, selectedProgrammer }, version });
this.fireChanged();
return true;
}
async selectConfigOption(
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { configOptions } = data;
const configOption = configOptions.find(c => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
}
}
if (!updated) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
this.fireChanged();
return true;
}
protected async setData(
{ fqbn, data, version }: { fqbn: string, data: BoardsDataStore.Data, version: Installable.Version }): Promise<void> {
const key = this.getStorageKey(fqbn, version);
return this.storageService.setData(key, data);
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (err instanceof Error && err.message.includes('loading board data') && err.message.includes('is not installed')) {
this.logger.warn(`The boards package is not installed for board with FQBN: ${fqbn}`);
} else {
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
}
return undefined;
}
}
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(fqbn: string): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
const boardsPackage = await this.boardsService.getContainerBoardPackage({ fqbn });
if (!boardsPackage) {
return undefined;
}
return boardsPackage.installedVersion;
}
}
export namespace BoardsDataStore {
export interface Data {
readonly configOptions: ConfigOption[];
readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer;
}
export namespace Data {
export const EMPTY: Data = {
configOptions: [],
programmers: []
};
}
}

View File

@@ -1,17 +1,17 @@
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';
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
export class BoardsListWidget extends ListWidget<BoardsPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
super({
id: BoardsListWidget.WIDGET_ID,
@@ -19,7 +19,7 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemLabel: (item: BoardsPackage) => item.name,
itemRenderer
});
}

View File

@@ -1,12 +1,19 @@
import { injectable, inject } from 'inversify';
import { injectable, inject, optional } 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 { StorageService } from '@theia/core/lib/browser/storage-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent, BoardsService } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
import { compareAnything } from '../theia/monaco/comparers';
interface BoardMatch {
readonly board: Board & Readonly<{ packageName: string }>;
readonly matches: monaco.filters.IMatch[] | undefined;
}
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
@@ -14,16 +21,18 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
@inject(ILogger)
protected logger: ILogger;
@optional()
@inject(MessageService)
protected messageService: MessageService;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
@inject(StorageService)
protected storageService: StorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -34,35 +43,57 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
protected _availablePorts: Port[] = [];
protected _availableBoards: AvailableBoard[] = [];
protected boardsService: BoardsService;
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
/**
* Event when the state of the attached/detached boards has changed. For instance, the user have detached a physical board.
*/
readonly onAttachedBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardsPackageInstalled = this.onBoardsPackageInstalledEmitter.event;
readonly onBoardsPackageUninstalled = this.onBoardsPackageUninstalledEmitter.event;
/**
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
* This even also fires, when the boards package was not available for the currently selected board,
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
* currently selected board.\
* This even also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.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);
/**
* When the FE connects to the BE, the BE stets the known boards and ports.\
* This is a DI workaround for not being able to inject the service into the client.
*/
async init(boardsService: BoardsService): Promise<void> {
this.boardsService = boardsService;
const [attachedBoards, availablePorts] = await Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]);
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
this._attachedBoards = event.newState.boards;
this.onAttachedBoardsChangedEmitter.fire(event);
this._availablePorts = event.newState.ports;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
protected async tryReconnect(): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
@@ -73,13 +104,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
// 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)) {
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
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))
selectedPort: board.port
};
return true;
}
@@ -90,21 +121,86 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
this.onBoardsPackageInstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
const { installedVersion, id } = event.pkg;
if (selectedBoard) {
const installedBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: installedBoard
};
}
}
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardUninstalledEmitter.fire(event);
this.onBoardsPackageUninstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
if (selectedBoard && selectedBoard.fqbn) {
const uninstalledBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
this.logger.info(`Board package ${event.pkg.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
const selectedBoardWithoutFqbn = {
name: selectedBoard.name
// No FQBN
};
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: selectedBoardWithoutFqbn
};
}
}
}
set boardsConfig(config: BoardsConfig.Config) {
this.doSetBoardsConfig(config);
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
}
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
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));
}
async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise<Array<Board & { packageName: string }>> {
const boards = await this.boardsService.allBoards({});
const coresFilter = !!cores && cores.length
? ((toFilter: { packageName: string }) => cores.some(core => core === toFilter.packageName))
: () => true;
if (!query) {
return boards.filter(coresFilter).sort(Board.compare);
}
const toMatch = ((toFilter: Board & { packageName: string }) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) })));
const compareEntries = (left: BoardMatch, right: BoardMatch, lookFor: string) => {
const leftMatches = left.matches || [];
const rightMatches = right.matches || [];
if (leftMatches.length && !rightMatches.length) {
return -1;
}
if (!leftMatches.length && rightMatches.length) {
return 1;
}
if (leftMatches.length === 0 && rightMatches.length === 0) {
return 0;
}
const leftLabel = left.board.name.replace(/\r?\n/g, ' ');
const rightLabel = right.board.name.replace(/\r?\n/g, ' ');
return compareAnything(leftLabel, rightLabel, lookFor);
}
const normalizedQuery = query.toLowerCase();
return boards
.filter(coresFilter)
.map(toMatch)
.filter(({ matches }) => !!matches)
.sort((left, right) => compareEntries(left, right, normalizedQuery))
.map(({ board }) => board);
}
get boardsConfig(): BoardsConfig.Config {
@@ -123,7 +219,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
if (!config.selectedBoard) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn('No boards selected.', { timeout: 3000 });
}
return false;
@@ -133,7 +229,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
/**
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
* `true` if `canVerify`, the board has an FQBN and the `config.selectedPort` is also set, hence can upload to board. Otherwise, `false`.
*/
canUploadTo(
config: BoardsConfig.Config | undefined = this.boardsConfig,
@@ -145,14 +241,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
const { name } = config.selectedBoard;
if (!config.selectedPort) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
}
return false;
}
if (!config.selectedBoard.fqbn) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
}
return false;
@@ -161,8 +257,93 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
return true;
}
protected saveState(): Promise<void> {
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
get availableBoards(): AvailableBoard[] {
return this._availableBoards;
}
protected async reconcileAvailableBoards(): Promise<void> {
const attachedBoards = this._attachedBoards;
const availablePorts = this._availablePorts;
// Unset the port on the user's config, if it is not available anymore.
if (this.boardsConfig.selectedPort && !availablePorts.some(port => Port.sameAs(port, this.boardsConfig.selectedPort))) {
this.doSetBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined });
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
}
const boardsConfig = this.boardsConfig;
const currentAvailableBoards = this._availableBoards;
const availableBoards: AvailableBoard[] = [];
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
for (const boardPort of availableBoardPorts) {
let state = AvailableBoard.State.incomplete; // Initial pessimism.
let board = attachedSerialBoards.find(({ port }) => Port.sameAs(boardPort, port));
if (board) {
state = AvailableBoard.State.recognized;
} else {
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
if (lastSelectedBoard) {
board = {
...lastSelectedBoard,
port: boardPort
};
state = AvailableBoard.State.guessed;
}
}
if (!board) {
availableBoards.push({ name: 'Unknown', port: boardPort, state });
} else {
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
availableBoards.push({ ...board, state, selected, port: boardPort });
}
}
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
availableBoards.push({
...boardsConfig.selectedBoard,
port: boardsConfig.selectedPort,
selected: true,
state: AvailableBoard.State.incomplete
});
}
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.compare);
let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length;
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
hasChanged = AvailableBoard.compare(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0;
}
if (hasChanged) {
this._availableBoards = sortedAvailableBoards;
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
}
}
protected async getLastSelectedBoardOnPort(port: Port | string | undefined): Promise<Board | undefined> {
if (!port) {
return undefined;
}
const key = this.getLastSelectedBoardOnPortKey(port);
return this.storageService.getData<Board>(key);
}
protected async saveState(): Promise<void> {
// We save the port with the selected board name/FQBN, to be able to guess a better board name.
// Required when the attached board belongs to a 3rd party boards package, and neither the name, nor
// the FQBN can be retrieved with a `board list` command.
// https://github.com/arduino/arduino-cli/issues/623
const { selectedBoard, selectedPort } = this.boardsConfig;
if (selectedBoard && selectedPort) {
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
await this.storageService.setData(key, selectedBoard);
}
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
}
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${typeof port === 'string' ? port : Port.toString(port)}`;
}
protected async loadState(): Promise<void> {
@@ -176,3 +357,74 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
}
/**
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
* If it has the selected board and a associated port, it can be used for `upload`. We render an available board for the user
* when it has the `port` set.
*/
export interface AvailableBoard extends Board {
readonly state: AvailableBoard.State;
readonly selected?: boolean;
readonly port?: Port;
}
export namespace AvailableBoard {
export enum State {
/**
* Retrieved from the CLI via the `board list` command.
*/
'recognized',
/**
* Guessed the name/FQBN of the board from the available board ports (3rd party).
*/
'guessed',
/**
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
*/
'incomplete'
}
export function is(board: any): board is AvailableBoard {
return Board.is(board) && 'state' in board;
}
export function hasPort(board: AvailableBoard): board is AvailableBoard & { port: Port } {
return !!board.port;
}
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
if (left.selected && !right.selected) {
return -1;
}
if (right.selected && !left.selected) {
return 1;
}
let result = naturalCompare(left.name, right.name);
if (result !== 0) {
return result;
}
if (left.fqbn && right.fqbn) {
result = naturalCompare(left.fqbn, right.fqbn);
if (result !== 0) {
return result;
}
}
if (left.port && right.port) {
result = Port.compare(left.port, right.port);
if (result !== 0) {
return result;
}
}
if (!!left.selected && !right.selected) {
return -1;
}
if (!!right.selected && !left.selected) {
return 1;
}
return left.state - right.state;
}
}

View File

@@ -1,10 +1,11 @@
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 { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl, AvailableBoard } from './boards-service-client-impl';
export interface BoardsDropDownListCoords {
readonly top: number;
@@ -16,14 +17,9 @@ export interface BoardsDropDownListCoords {
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
@@ -51,48 +47,30 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.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)}
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig()
})}
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
{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';
@@ -102,10 +80,9 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
constructor(props: BoardsToolBarItem.Props) {
super(props);
const { availableBoards } = props.boardsServiceClient;
this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
availableBoards,
coords: 'hidden'
};
@@ -115,17 +92,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
}
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 })
});
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
}
componentWillUnmount(): void {
@@ -146,7 +113,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
}
});
} else {
this.setState({ coords: 'hidden'});
this.setState({ coords: 'hidden' });
}
}
event.stopPropagation();
@@ -154,41 +121,52 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
};
render(): React.ReactNode {
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
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))
}
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached'
}
}));
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed'
}
return ''
})();
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' : ''}/>
<span className={decorator} />
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{title}
</div>
</div>
<span className='fa fa-caret-down caret'/>
<span className='fa fa-caret-down caret' />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={items}
items={availableBoards.filter(AvailableBoard.hasPort).map(board => ({
...board,
onClick: () => {
if (board.state === AvailableBoard.State.incomplete) {
this.props.boardsServiceClient.boardsConfig = {
selectedPort: board.port
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: board.port
}
}
}
}))}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
@@ -200,3 +178,16 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
};
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
availableBoards: AvailableBoard[];
coords: BoardsDropDownListCoords | 'hidden';
}
}

View File

@@ -1,12 +1,12 @@
import { injectable } from 'inversify';
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';
import { BoardsPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
@@ -19,7 +19,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
rank: 600
},
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleKeybinding: 'ctrlcmd+shift+b'
toggleKeybinding: 'CtrlCmd+Shift+B'
});
}
@@ -29,9 +29,10 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...'
label: 'Boards Manager...',
order: '4'
});
}
}

View File

@@ -0,0 +1,16 @@
import { ContainerModule } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandContribution } from '@theia/core/lib/common/command';
import { QuickOpenContribution } from '@theia/core/lib/browser/quick-open';
import { KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { BoardsQuickOpenService } from './boards-quick-open-service';
export default new ContainerModule(bind => {
bind(BoardsQuickOpenService).toSelf().inSingletonScope();
bind(CommandContribution).toService(BoardsQuickOpenService);
bind(KeybindingContribution).toService(BoardsQuickOpenService);
bind(QuickOpenContribution).toService(BoardsQuickOpenService);
bind(ILogger).toDynamicValue(({ container }) => container.get<ILogger>(ILogger).child('boards-quick-open'))
.inSingletonScope()
.whenTargetNamed('boards-quick-open');
});

View File

@@ -0,0 +1,309 @@
import * as fuzzy from 'fuzzy';
import { inject, injectable, postConstruct, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { QuickOpenItem, QuickOpenModel, QuickOpenMode, QuickOpenGroupItem } from '@theia/core/lib/common/quick-open-model';
import {
QuickOpenService,
QuickOpenHandler,
QuickOpenOptions,
QuickOpenItemOptions,
QuickOpenContribution,
QuickOpenActionProvider,
QuickOpenHandlerRegistry,
QuickOpenGroupItemOptions
} from '@theia/core/lib/browser/quick-open';
import { naturalCompare } from '../../../common/utils';
import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol';
import { CoreServiceClientImpl } from '../../core-service-client-impl';
import { BoardsDataStore } from '../boards-data-store';
import { BoardsServiceClientImpl, AvailableBoard } from '../boards-service-client-impl';
@injectable()
export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command {
readonly id = 'arduino-boards-quick-open';
readonly prefix = '|';
readonly description = 'Configure Available Boards';
readonly label: 'Configure Available Boards';
@inject(ILogger)
@named('boards-quick-open')
protected readonly logger: ILogger;
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(CoreServiceClientImpl)
protected coreServiceClient: CoreServiceClientImpl;
protected isOpen: boolean = false;
protected currentQuery: string = '';
// Attached boards plus the user's config.
protected availableBoards: AvailableBoard[] = [];
// Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional.
protected data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY;
protected allBoards: Board.Detailed[] = []
protected selectedBoard?: (AvailableBoard & { port: Port });
// `init` name is used by the `QuickOpenHandler`.
@postConstruct()
protected postConstruct(): void {
this.coreServiceClient.onIndexUpdated(() => this.update(this.availableBoards));
this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards));
this.update(this.boardsServiceClient.availableBoards);
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(this, { execute: () => this.open() });
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ command: this.id, keybinding: 'ctrlCmd+k ctrlCmd+b' });
}
registerQuickOpenHandlers(registry: QuickOpenHandlerRegistry): void {
registry.registerHandler(this);
}
getModel(): QuickOpenModel {
return this;
}
getOptions(): QuickOpenOptions {
let placeholder = '';
if (!this.selectedBoard) {
placeholder += 'No board selected.';
}
placeholder += 'Type to filter boards';
if (this.data.configOptions.length) {
placeholder += ' or use the ↓↑ keys to adjust the board settings...';
} else {
placeholder += '...';
}
return {
placeholder,
fuzzyMatchLabel: true,
onClose: () => this.isOpen = false
};
}
open(): void {
this.isOpen = true;
this.quickOpenService.open(this, this.getOptions());
}
onType(
lookFor: string,
acceptor: (items: QuickOpenItem<QuickOpenItemOptions>[], actionProvider?: QuickOpenActionProvider) => void): void {
this.currentQuery = lookFor;
const fuzzyFilter = this.fuzzyFilter(lookFor);
const availableBoards = this.availableBoards.filter(AvailableBoard.hasPort).filter(({ name }) => fuzzyFilter(name));
const toAccept: QuickOpenItem<QuickOpenItemOptions>[] = [];
// Show the selected attached in a different group.
if (this.selectedBoard && fuzzyFilter(this.selectedBoard.name)) {
toAccept.push(this.toQuickItem(this.selectedBoard, { groupLabel: 'Selected Board' }));
}
// Filter the selected from the attached ones.
toAccept.push(...availableBoards.filter(board => board !== this.selectedBoard).map((board, i) => {
let group: QuickOpenGroupItemOptions | undefined = undefined;
if (i === 0) {
// If no `selectedBoard`, then this item is the top one, no borders required.
group = { groupLabel: 'Attached Boards', showBorder: !!this.selectedBoard };
}
return this.toQuickItem(board, group);
}));
// Show the config only if the `input` is empty.
if (!lookFor.trim().length) {
toAccept.push(...this.data.configOptions.map((config, i) => {
let group: QuickOpenGroupItemOptions | undefined = undefined;
if (i === 0) {
group = { groupLabel: 'Board Settings', showBorder: true };
}
return this.toQuickItem(config, group);
}));
} else {
toAccept.push(...this.allBoards.filter(({ name }) => fuzzyFilter(name)).map((board, i) => {
let group: QuickOpenGroupItemOptions | undefined = undefined;
if (i === 0) {
group = { groupLabel: 'Boards', showBorder: true };
}
return this.toQuickItem(board, group);
}));
}
acceptor(toAccept);
}
private fuzzyFilter(lookFor: string): (inputString: string) => boolean {
const shouldFilter = !!lookFor.trim().length;
return (inputString: string) => shouldFilter ? fuzzy.test(lookFor.toLocaleLowerCase(), inputString.toLocaleLowerCase()) : true;
}
protected async update(availableBoards: AvailableBoard[]): Promise<void> {
// `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!)
const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected);
const [data, boards] = await Promise.all([
selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY),
this.boardsService.allBoards({})
]);
this.allBoards = Board.decorateBoards(selectedBoard, boards)
.filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board)));
this.availableBoards = availableBoards;
this.data = data;
this.selectedBoard = selectedBoard;
if (this.isOpen) {
// Hack, to update the state without closing and reopening the quick open widget.
(this.quickOpenService as any).onType(this.currentQuery);
}
}
protected toQuickItem(item: BoardsQuickOpenService.Item, group?: QuickOpenGroupItemOptions): QuickOpenItem<QuickOpenItemOptions> {
let options: QuickOpenItemOptions;
if (AvailableBoard.is(item)) {
const description = `on ${Port.toString(item.port)}`
options = {
label: `${item.name}`,
description,
descriptionHighlights: [
{
start: 0,
end: description.length
}
],
run: this.toRun(() => this.boardsServiceClient.boardsConfig = ({ selectedBoard: item, selectedPort: item.port }))
};
} else if (ConfigOption.is(item)) {
const selected = item.values.find(({ selected }) => selected);
const value = selected ? selected.label : 'Not set';
const label = `${item.label}: ${value}`;
options = {
label,
// Intended to match the value part of a board setting.
// NOTE: this does not work, as `fuzzyMatchLabel: true` is set. Manual highlighting is ignored, apparently.
labelHighlights: [
{
start: label.length - value.length,
end: label.length
}
],
run: (mode) => {
if (mode === QuickOpenMode.OPEN) {
this.setConfig(item);
return false;
}
return true;
}
};
if (!selected) {
options.description = 'Not set';
};
} else {
options = {
label: `${item.name}`,
description: `${item.missing ? '' : `[installed with '${item.packageName}']`}`,
run: (mode) => {
if (mode === QuickOpenMode.OPEN) {
this.selectBoard(item);
return false;
}
return true;
}
};
}
if (group) {
return new QuickOpenGroupItem<QuickOpenGroupItemOptions>({ ...options, ...group });
} else {
return new QuickOpenItem<QuickOpenItemOptions>(options);
}
}
protected toRun(run: (() => void)): ((mode: QuickOpenMode) => boolean) {
return (mode) => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
run();
return true;
};
}
protected async selectBoard(board: Board): Promise<void> {
const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare);
const toItem = (port: Port) => new QuickOpenItem<QuickOpenItemOptions>({
label: Port.toString(port, { useLabel: true }),
run: this.toRun(() => {
this.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: port
};
})
});
const options = {
placeholder: `Select a port for '${board.name}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
fuzzyMatchLabel: true
}
this.quickOpenService.open({
onType: (lookFor, acceptor) => {
const fuzzyFilter = this.fuzzyFilter(lookFor);
acceptor(allPorts.filter(({ address }) => fuzzyFilter(address)).map(toItem));
}
}, options);
}
protected async setConfig(config: ConfigOption): Promise<void> {
const toItem = (value: ConfigValue) => new QuickOpenItem<QuickOpenItemOptions>({
label: value.label,
iconClass: value.selected ? 'fa fa-check' : '',
run: this.toRun(() => {
if (!this.selectedBoard) {
this.logger.warn(`Could not alter the boards settings. No board selected. ${JSON.stringify(config)}`);
return;
}
if (!this.selectedBoard.fqbn) {
this.logger.warn(`Could not alter the boards settings. The selected board does not have a FQBN. ${JSON.stringify(this.selectedBoard)}`);
return;
}
const { fqbn } = this.selectedBoard;
this.boardsDataStore.selectConfigOption({
fqbn,
option: config.option,
selectedValue: value.value
});
})
});
const options = {
placeholder: `Configure '${config.label}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
fuzzyMatchLabel: true
}
this.quickOpenService.open({
onType: (lookFor, acceptor) => {
const fuzzyFilter = this.fuzzyFilter(lookFor);
acceptor(config.values
.filter(({ label }) => fuzzyFilter(label))
.sort((left, right) => naturalCompare(left.label, right.label))
.map(toItem));
}
}, options);
}
}
export namespace BoardsQuickOpenService {
export type Item = AvailableBoard & { port: Port } | Board.Detailed | ConfigOption;
}

View File

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

View File

@@ -0,0 +1,89 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { EditorManager } from '@theia/editor/lib/browser';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
@injectable()
export class CloseSketch extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
execute: async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && await this.wasTouched(uri)) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message: 'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost."
});
if (response === 1) { // Cancel
return;
}
if (response === 2) { // Save
const saved = await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { openAfterMove: false, execOnlyIfTemp: true });
if (!saved) { // If it was not saved, do bail the close.
return;
}
}
}
window.close();
}
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: CloseSketch.Commands.CLOSE_SKETCH.id,
label: 'Close',
order: '5'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: CloseSketch.Commands.CLOSE_SKETCH.id,
keybinding: 'CtrlCmd+W'
});
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
protected async wasTouched(uri: string): Promise<boolean> {
const editorWidget = await this.editorManager.getByUri(new URI(uri));
if (editorWidget) {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor.getControl().getModel()?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
return true;
}
}
}
return false;
}
}
export namespace CloseSketch {
export namespace Commands {
export const CLOSE_SKETCH: Command = {
id: 'arduino-close-sketch'
};
}
}

View File

@@ -0,0 +1,85 @@
import { inject, injectable, interfaces } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileSystem } from '@theia/filesystem/lib/common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu';
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@injectable()
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
registerCommands(registry: CommandRegistry): void {
}
registerMenus(registry: MenuModelRegistry): void {
}
registerKeybindings(registry: KeybindingRegistry): void {
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
}
}
@injectable()
export abstract class SketchContribution extends Contribution {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
}
export namespace Contribution {
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: interfaces.ServiceIdentifier<T>): void {
bind(serviceIdentifier).toSelf().inSingletonScope();
bind(CommandContribution).toService(serviceIdentifier);
bind(MenuContribution).toService(serviceIdentifier);
bind(KeybindingContribution).toService(serviceIdentifier);
bind(TabBarToolbarContribution).toService(serviceIdentifier);
}
}

View File

@@ -0,0 +1,277 @@
import { inject, injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { EDITOR_FONT_DEFAULTS } from '@theia/editor/lib/browser/editor-preferences';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable()
export class EditContributions extends Contribution {
@inject(MonacoEditorService)
protected readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { execute: () => this.run('editor.action.gotoLine') });
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, { execute: () => this.run('editor.action.commentLine') });
registry.registerCommand(EditContributions.Commands.INDENT_LINES, { execute: () => this.run('editor.action.indentLines') });
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, { execute: () => this.run('editor.action.outdentLines') });
registry.registerCommand(EditContributions.Commands.FIND, { execute: () => this.run('actions.find') });
registry.registerCommand(EditContributions.Commands.FIND_NEXT, { execute: () => this.run('actions.findWithSelection') });
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { execute: () => this.run('editor.action.nextMatchFindAction') });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) + 1)
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) - 1)
});
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`[code]
${value}
[/code]`)
}
}
});
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`\`\`\`cpp
${value}
\`\`\``)
}
}
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.COPY.id,
order: '1'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
order: '2'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
order: '3'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
order: '4'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5'
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6'
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
order: '2'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: 'Decrease Font Size',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
order: '2'
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
order: '3'
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
order: '0'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+='
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-'
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F'
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_NEXT.id,
keybinding: 'CtrlCmd+G'
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_PREVIOUS.id,
keybinding: 'CtrlCmd+Shift+G'
});
registry.registerKeybinding({
command: EditContributions.Commands.USE_FOR_FIND.id,
keybinding: 'CtrlCmd+E'
});
// `Tools`
registry.registerKeybinding({
command: EditContributions.Commands.AUTO_FORMAT.id,
keybinding: 'CtrlCmd+T'
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
return this.codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getActiveCodeEditor();
}
protected async currentValue(): Promise<string | undefined> {
const currentEditor = await this.current();
if (currentEditor) {
const selection = currentEditor.getSelection();
if (!selection || selection.isEmpty()) {
return currentEditor.getValue();
}
return currentEditor.getModel()?.getValueInRange(selection);
}
return undefined;
}
protected async run(commandId: string): Promise<any> {
const editor = await this.current();
if (editor) {
const action = editor.getAction(commandId);
if (action) {
return action.run();
}
}
}
}
export namespace EditContributions {
export namespace Commands {
export const COPY_FOR_FORUM: Command = {
id: 'arduino-copy-for-forum'
};
export const COPY_FOR_GITHUB: Command = {
id: 'arduino-copy-for-github'
};
export const GO_TO_LINE: Command = {
id: 'arduino-go-to-line'
};
export const TOGGLE_COMMENT: Command = {
id: 'arduino-toggle-comment'
};
export const INDENT_LINES: Command = {
id: 'arduino-indent-lines'
};
export const OUTDENT_LINES: Command = {
id: 'arduino-outdent-lines'
};
export const FIND: Command = {
id: 'arduino-find'
};
export const FIND_NEXT: Command = {
id: 'arduino-find-next'
};
export const FIND_PREVIOUS: Command = {
id: 'arduino-find-previous'
};
export const USE_FOR_FIND: Command = {
id: 'arduino-for-find'
};
export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size'
};
export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size'
};
export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format' // `Auto Format` should belong to `Tool`.
};
}
}

View File

@@ -0,0 +1,63 @@
import { injectable } from 'inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch()
});
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: 'New',
order: '0'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
tooltip: 'New',
priority: 3
});
}
async newSketch(): Promise<void> {
try {
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
await this.messageService.error(e.toString());
}
}
}
export namespace NewSketch {
export namespace Commands {
export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch'
};
export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar'
};
}
}

View File

@@ -0,0 +1,51 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
@injectable()
export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: 'Show Sketch Folder',
order: '0'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K'
});
}
protected async openExternal(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) {
const exists = this.fileSystem.exists(uri);
if (exists) {
const fsPath = await this.fileSystem.getFsPath(uri);
if (fsPath) {
remote.shell.showItemInFolder(fsPath);
}
}
}
}
}
export namespace OpenSketchExternal {
export namespace Commands {
export const OPEN_EXTERNAL: Command = {
id: 'arduino-open-sketch-external'
};
}
}

View File

@@ -0,0 +1,163 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
export class OpenSketch extends SketchContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: arg => Sketch.is(arg) ? this.openSketch(arg) : this.openSketch()
});
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => {
const sketches = await this.sketchService.getSketches();
if (!sketches.length) {
this.openSketch();
} else {
this.toDisposeBeforeCreateNewContextMenu.dispose();
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...'
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
for (const sketch of sketches) {
const command = { id: `arduino-open-sketch--${sketch.uri}` };
const handler = { execute: () => this.openSketch(sketch) };
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, {
commandId: command.id,
label: sketch.name
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
}
}
this.contextMenuRenderer.render(options);
}
}
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
order: '1'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
priority: 4
});
}
async openSketch(toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
}
}
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileSystem.getFsPath(config.sketchDirUri);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino']
}
]
});
if (!filePaths.length) {
return undefined;
}
if (filePaths.length > 1) {
this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`);
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (sketchFileUri.endsWith('.ino')) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: 'Moving',
type: 'question',
buttons: ['Cancel', 'OK'],
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`
});
if (response === 1) { // OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileSystem.exists(newSketchUri.toString());
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: 'Error',
message: `A folder named "${name}" already exists. Can't open sketch.`
});
return undefined;
}
await this.fileSystem.createFolder(newSketchUri.toString());
await this.fileSystem.move(sketchFileUri, newSketchUri.resolve(nameWithExt).toString());
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
}
export namespace OpenSketch {
export namespace Commands {
export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch'
};
export const OPEN_SKETCH__TOOLBAR: Command = {
id: 'arduino-open-sketch--toolbar'
};
}
}

View File

@@ -0,0 +1,46 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { isOSX } from '@theia/core/lib/common/os';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void {
if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit()
});
}
}
registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
commandId: QuitApp.Commands.QUIT_APP.id,
label: 'Quit',
order: '0'
});
}
}
registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) {
registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id,
keybinding: 'CtrlCmd+Q'
});
}
}
}
export namespace QuitApp {
export namespace Commands {
export const QUIT_APP: Command = {
id: 'arduino-quit-app'
};
}
}

View File

@@ -0,0 +1,94 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
@injectable()
export class SaveAsSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: args => this.saveAs(args)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: 'Save As...',
order: '7'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S'
});
}
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs({ execOnlyIfTemp, openAfterMove, wipeOriginal }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return false;
}
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri);
const exists = await this.fileSystem.exists(sketchDirUri.resolve(sketch.name).toString());
const defaultUri = exists
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileSystem.getFsPath(defaultUri.toString())!;
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return false;
}
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
if (workspaceUri && openAfterMove) {
if (wipeOriginal) {
await this.fileSystem.delete(sketch.uri);
}
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
}
return !!workspaceUri;
}
}
export namespace SaveAsSketch {
export namespace Commands {
export const SAVE_AS_SKETCH: Command = {
id: 'arduino-save-as-sketch'
};
}
export interface Options {
readonly execOnlyIfTemp?: boolean;
readonly openAfterMove?: boolean;
/**
* Ignored if `openAfterMove` is `false`.
*/
readonly wipeOriginal?: boolean;
}
export namespace Options {
export const DEFAULT: Options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false
};
}
}

View File

@@ -0,0 +1,59 @@
import { injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch()
});
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: 'Save',
order: '6'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
tooltip: 'Save',
priority: 5
});
}
async saveSketch(): Promise<void> {
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
}
}
export namespace SaveSketch {
export namespace Commands {
export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch'
};
export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar'
};
}
}

View File

@@ -0,0 +1,38 @@
import { injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { URI, Command, MenuModelRegistry, CommandRegistry, SketchContribution, open } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class Settings extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN_CLI_CONFIG, {
execute: () => this.configService.getCliConfigFileUri().then(uri => open(this.openerService, new URI(uri)))
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: CommonCommands.OPEN_PREFERENCES.id,
label: 'Preferences...',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: Settings.Commands.OPEN_CLI_CONFIG.id,
label: 'Open CLI Configuration',
order: '1',
});
}
}
export namespace Settings {
export namespace Commands {
export const OPEN_CLI_CONFIG: Command = {
id: 'arduino-open-cli-config',
label: 'Open CLI Configuration',
category: 'Arduino'
}
}
}

View File

@@ -0,0 +1,129 @@
import { inject, injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { URI, SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, open } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class SketchControl extends SketchContribution {
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, {
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const target = document.getElementById(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id);
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
const command = { id: `arduino-focus-file--${uri.toString()}` };
const handler = { execute: () => open(this.openerService, uri) };
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP, {
commandId: command.id,
label: this.labelProvider.getName(uri),
order: `${i}`
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
const options = {
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
}
}
this.contextMenuRenderer.render(options);
}
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
commandId: WorkspaceCommands.FILE_RENAME.id,
label: 'Rename',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete',
order: '2'
});
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
order: '0'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N'
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left' // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
keybinding: 'CtrlCmd+Alt+Right'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
});
}
}
export namespace SketchControl {
export namespace Commands {
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
id: 'arduino-open-sketch-control--toolbar',
iconClass: 'fa fa-caret-down'
};
}
}

View File

@@ -0,0 +1,159 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: () => this.uploadSketch()
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, {
execute: () => this.uploadSketch(true)
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '1'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U'
});
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
keybinding: 'CtrlCmd+Shift+U'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
tooltip: 'Upload',
priority: 1
});
}
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const [fqbn, { selectedProgrammer }] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard.fqbn)
]);
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
if (usingProgrammer) {
const programmer = selectedProgrammer;
if (!programmer) {
throw new Error('Programmer is not selected. Please select a programmer.');
}
let port: undefined | string = undefined;
// If the port is set by the user, we pass it to the CLI as it might be required.
// If it is not set but the CLI requires it, we let the CLI to complain.
if (selectedPort) {
port = selectedPort.address;
}
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer,
port
};
} else {
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
const port = selectedPort.address;
options = {
sketchUri,
fqbn,
optimizeForDebug,
port
};
}
this.outputChannelManager.getChannel('Arduino: upload').clear();
await this.coreService.upload(options);
this.messageService.info('Done uploading.', { timeout: 1000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
}
}
export namespace UploadSketch {
export namespace Commands {
export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch'
};
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
id: 'arduino-upload-sketch-using-programmer'
};
export const UPLOAD_SKETCH_TOOLBAR: Command = {
id: 'arduino-upload-sketch--toolbar'
};
}
}

View File

@@ -0,0 +1,96 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
@injectable()
export class VerifySketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch()
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
order: '2'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
tooltip: 'Verify',
priority: 0
});
}
async verifySketch(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
this.outputChannelManager.getChannel('Arduino: compile').clear();
await this.coreService.compile({
sketchUri: uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug
});
this.messageService.info('Done compiling.', { timeout: 1000 });
} catch (e) {
this.messageService.error(e.toString());
}
}
}
export namespace VerifySketch {
export namespace Commands {
export const VERIFY_SKETCH: Command = {
id: 'arduino-verify-sketch'
};
export const VERIFY_SKETCH_TOOLBAR: Command = {
id: 'arduino-verify-sketch--toolbar'
};
}
}

View File

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

View File

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

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

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

View File

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

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

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

@@ -108,7 +108,9 @@
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#00979d",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1"
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#FFFFFF",
"arduino.output.background": "#000000"
},
"type": "light",
"name": "Arduino"

View File

@@ -1,16 +1,16 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { injectable, inject } from 'inversify';
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 { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsListWidget } from './boards/boards-list-widget';
import { LibraryListWidget } from './library/library-list-widget';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
readonly menuContentChanged = new Emitter<void>();
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
protected app: FrontendApplication;
@@ -41,7 +41,7 @@ export class EditorMode implements FrontendApplicationContribution {
// `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
const layoutRestorer = (this.app as any).layoutRestorer as { storeLayoutAsync(app: FrontendApplication): Promise<void> };
await layoutRestorer.storeLayoutAsync(this.app);
window.location.reload(true);
}
@@ -62,6 +62,7 @@ export class EditorMode implements FrontendApplicationContribution {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
this.mainMenuManager.update();
}
}

View File

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

@@ -2,7 +2,6 @@ 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 {
@@ -26,18 +25,6 @@ export class ArduinoLanguageClientContribution extends BaseLanguageClientContrib
@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 {

View File

@@ -1,7 +1,7 @@
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';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {

View File

@@ -3,7 +3,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class LibraryListWidgetFrontendContribution extends AbstractViewContribution<LibraryListWidget> implements FrontendApplicationContribution {
@@ -17,7 +17,7 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut
rank: 700
},
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'ctrlcmd+shift+l'
toggleKeybinding: 'CtrlCmd+Shift+I'
});
}
@@ -27,9 +27,10 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Manage Libraries...'
label: 'Manage Libraries...',
order: '3'
});
}
}

View File

@@ -0,0 +1,52 @@
import { MAIN_MENU_BAR } from '@theia/core/lib/common/menu';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { isOSX } from '@theia/core';
export namespace ArduinoMenus {
// Main menu
// -- File
export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch'];
export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print'];
// XXX: on macOS, the settings group is not under `File`
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
// -- Edit
// `Copy`, `Copy to Forum`, `Paste`, etc.
// Note: `1_undo` is the first group from Theia, we start with `2`
export const EDIT__TEXT_CONTROL_GROUP = [...CommonMenus.EDIT, '2_text_control'];
// `Comment/Uncomment`, etc.
export const EDIT__CODE_CONTROL_GROUP = [...CommonMenus.EDIT, '3_code_control'];
export const EDIT__FONT_CONTROL_GROUP = [...CommonMenus.EDIT, '4_font_control'];
export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find'];
// -- Sketch
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main'];
export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils'];
// -- Tools
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// Core settings, such as `Processor` and `Programmers` for the board.
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '1_board_settings'];
// Context menu
// -- Open
export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context'];
export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [...OPEN_SKETCH__CONTEXT, '0_open'];
export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [...OPEN_SKETCH__CONTEXT, '1_recent'];
export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [...OPEN_SKETCH__CONTEXT, '2_examples'];
// -- Sketch control
export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context'];
// `New Tab`, `Rename`, `Delete`
export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [...SKETCH_CONTROL__CONTEXT, '0_main'];
// `Previous Tab`, `Next Tab`
export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [...SKETCH_CONTROL__CONTEXT, '1_navigation'];
// Sketch files opened in editors
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [...SKETCH_CONTROL__CONTEXT, '2_resources'];
}

View File

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

@@ -1,10 +1,11 @@
import { injectable, inject, postConstruct } from 'inversify';
import { deepClone } from '@theia/core/lib/common/objects';
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 { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
@@ -110,12 +111,12 @@ export class MonitorConnection {
}
});
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.boardsServiceClient.onBoardsChanged(event => {
this.boardsServiceClient.onAttachedBoardsChanged(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))) {
if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect()
@@ -182,15 +183,19 @@ export class MonitorConnection {
}
async disconnect(): Promise<Status> {
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
if (!this.connected) {
return Status.OK;
}
const stateCopy = deepClone(this.state);
if (!stateCopy) {
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)}`);
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(stateCopy)}`);
} else {
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(stateCopy)}`);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
@@ -225,7 +230,7 @@ export class MonitorConnection {
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 }) => {
this.boardsService.getAvailablePorts().then(ports => {
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
new Promise<void>(resolve => {
// First, disconnect if connected.

View File

@@ -1,12 +1,12 @@
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 { injectable, inject } from 'inversify';
import { AbstractViewContribution } from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { MonitorModel } from './monitor-model';
import { ArduinoMenus } from '../menu/arduino-menus';
export namespace SerialMonitor {
export namespace Commands {
@@ -42,15 +42,16 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'ctrlcmd+shift+m'
toggleKeybinding: 'CtrlCmd+Shift+M'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
label: 'Serial Monitor',
order: '5'
});
}
}
@@ -86,25 +87,23 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
}
});
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, {
commands.registerCommand(this.toggleCommand, { execute: () => this.toggle() });
commands.registerCommand({ id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR }, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
execute: () => this.openView({
toggle: true,
activate: true
})
execute: () => this.toggle()
});
}
}
protected async toggle(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
} else {
await this.openView({ activate: true, reveal: true });
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div

View File

@@ -9,7 +9,7 @@ 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 { ArduinoSelect } from '../widgets/arduino-select';
import { MonitorModel } from './monitor-model';
import { MonitorConnection } from './monitor-connection';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@@ -178,7 +178,7 @@ export class MonitorWidget extends ReactWidget {
this.monitorModel.lineEnding = option.value;
}
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
protected readonly onChangeBaudRate = (option: SelectOption<MonitorConfig.BaudRate>) => {
this.monitorModel.baudRate = option.value;
}
@@ -288,17 +288,20 @@ export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Pro
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 });
const rawLines = data.split('\n');
const lines: string[] = []
const timestamp = () => this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : '';
for (let i = 0; i < rawLines.length; i++) {
if (i === 0 && this.state.content.length !== 0) {
lines.push(rawLines[i]);
} else {
lines.push(timestamp() + rawLines[i]);
}
}
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {

View File

@@ -97,7 +97,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
margin-left: auto;
}
#select-board-dialog .selectBoardContainer .body .list .item .detail {
#select-board-dialog .selectBoardContainer .body .list .item .details {
font-size: var(--theia-ui-font-size1);
opacity: var(--theia-mod-disabled-opacity);
width: 155px; /* used heuristics for the calculation */
@@ -169,6 +169,13 @@ button.theia-button.main {
margin: 0 5px;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .guessed {
width: 10px;
height: 10px;
color: var(--theia-warningBackground);
margin: 0 5px;
}
.arduino-boards-toolbar-item-container {
display: flex;
align-items: center;

View File

@@ -1,13 +0,0 @@
/* Do not show the `close` icon for editor, but show the dirty state if not in pro-mode. */
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon {
background-image: none;
cursor: pointer;
}
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
background-size: 10px;
background-image: var(--theia-icon-circle);
cursor: pointer;
}

View File

@@ -1,9 +1,10 @@
@import './list-widget.css';
@import './board-select-dialog.css';
@import './boards-config-dialog.css';
@import './main.css';
@import './editor.css';
@import './monitor.css';
@import './arduino-select.css';
@import './status-bar.css';
@import './terminal.css';
.theia-input.warning:focus {
outline-width: 1px;

View File

@@ -46,7 +46,7 @@
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
https://github.com/arduino/arduino-pro-ide/issues/82 */
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
.arduino-list-widget .filterable-list-container .items-container .ps__rail-y {
z-index: 1;
}

View File

@@ -2,15 +2,6 @@
background: var(--theia-editorGroupHeader-tabsBackground);
}
.theia-output {
background: var(--theia-terminal-background);
color: var(--theia-terminal-foreground);
}
#outputView {
cursor: text;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 3px;
}
@@ -21,14 +12,15 @@
align-items: center;
height: 24px;
width: 24px;
background: var(--theia-button-background);
background: var(--theia-arduino-toolbar-background);
}
.p-TabBar-toolbar .item.arduino-tool-item > div:hover {
background: var(--theia-button-hoverBackground);
background: (--theia-arduino-toolbar-hoverBackground);
}
.arduino-verify, .arduino-upload {
.arduino-verify-sketch--toolbar,
.arduino-upload-sketch--toolbar {
border-radius: 12px;
}
@@ -42,22 +34,27 @@
mask-size: 800%;
}
.arduino-save-file-icon {
.arduino-save-sketch--toolbar-icon {
-webkit-mask-position: 59px -4px;
mask-position: 59px -4px;
}
.arduino-verify-icon {
.arduino-verify-sketch--toolbar-icon {
-webkit-mask-position: 188px -4px;
mask-position: 188px -4px;
}
.arduino-upload-icon {
.arduino-upload-sketch--toolbar-icon {
-webkit-mask-position: 156px -4px;
mask-position: 156px -4px;
}
.arduino-show-open-context-menu-icon {
.arduino-new-sketch--toolbar-icon {
-webkit-mask-position: 124px -4px;
mask-position: 124px -4px;
}
.arduino-open-sketch--toolbar-icon {
-webkit-mask-position: 92px -4px;
mask-position: 92px -4px;
}
@@ -117,7 +114,17 @@
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-titleBar-activeBackground);;
color: var(--theia-titleBar-activeBackground);
}
.arduino-open-boards-control-icon {
mask: none;
-webkit-mask: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-titleBar-activeBackground);
}
.monaco-editor .margin {
@@ -138,3 +145,33 @@
.p-Widget.dialogOverlay .dialogBlock {
background-color: var(--theia-arduino-foreground);
}
#arduino-open-sketch-control--toolbar--container {
background-color: var(--theia-arduino-toolbar-background);
padding: 8px 8px 8px 8px; /* based on pure heuristics */
}
#arduino-open-sketch-control--toolbar {
height: unset;
width: unset;
line-height: unset;
color: var(--theia-titleBar-activeBackground);
}
/* Output */
.theia-output .editor-container {
background-color: var(--theia-arduino-output-background);
}
.theia-output .monaco-editor .lines-content.monaco-editor-background {
background-color: var(--theia-arduino-output-background);
}
.theia-output .monaco-editor .lines-content.monaco-editor-background .view-lines .view-line .mtk1:not(.theia-output-error):not(.theia-output-warning) {
color: var(--theia-arduino-output-foreground);
}
.theia-output .monaco-editor .margin {
border-right: none;
background-color: var(--theia-arduino-output-background);
}

View File

@@ -0,0 +1,7 @@
#theia-statusBar .area .element.arduino-selected-port {
margin-left: 0px;
}
#theia-statusBar .area .element.arduino-selected-board > *:last-child {
margin-right: 0px;
}

View File

@@ -0,0 +1,3 @@
.terminal-container .xterm .xterm-helper-textarea {
opacity: 0 !important; /* fix secondary cursor-like issue. See https://github.com/eclipse-theia/theia/issues/8158 */
}

View File

@@ -1,9 +1,9 @@
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';
import { AboutDialog as TheiaAboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../../common/protocol/config-service';
@injectable()
export class ArduinoAboutDialog extends AboutDialog {
export class AboutDialog extends TheiaAboutDialog {
@inject(ConfigService)
protected readonly configService: ConfigService;

View File

@@ -0,0 +1,68 @@
import { injectable, inject } from 'inversify';
import { EditorWidget } from '@theia/editor/lib/browser';
import { CommandService } from '@theia/core/lib/common/command';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { ApplicationShell as TheiaApplicationShell, Widget } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { EditorMode } from '../../editor-mode';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
protected track(widget: Widget): void {
super.track(widget);
if (widget instanceof OutputWidget) {
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
}
if (!this.editorMode.proMode && widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then(sketch => {
if (sketch) {
if (Sketch.isInSketch(widget.editor.uri, sketch)) {
widget.title.closable = false;
}
}
});
}
}
async addWidget(widget: Widget, options: Readonly<TheiaApplicationShell.WidgetOptions> = {}): Promise<void> {
// By default, Theia open a widget **next** to the currently active in the target area.
// Instead of this logic, we want to open the new widget after the last of the target area.
if (!widget.id) {
console.error('Widgets added to the application shell must have a unique id property.');
return;
}
let ref: Widget | undefined = options.ref;
let area: TheiaApplicationShell.Area = options.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area);
if (tabBar) {
const last = tabBar.titles[tabBar.titles.length - 1];
if (last) {
ref = last.owner;
}
}
}
return super.addWidget(widget, { ...options, ref });
}
async saveAll(): Promise<void> {
await super.saveAll();
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
}
}

View File

@@ -0,0 +1,22 @@
import { injectable } from 'inversify';
import { BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, MenuBarWidget } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
@injectable()
export class BrowserMainMenuFactory extends TheiaBrowserMainMenuFactory implements MainMenuManager {
protected menuBar: MenuBarWidget | undefined;
createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar();
return this.menuBar;
}
update() {
if (this.menuBar) {
this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar);
}
}
}

View File

@@ -0,0 +1,13 @@
import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from 'inversify';
import { BrowserMenuBarContribution, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoMenuContribution } from './browser-menu-plugin';
import { BrowserMainMenuFactory } from './browser-main-menu-factory';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(BrowserMainMenuFactory);
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
rebind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
});

View File

@@ -0,0 +1,28 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CommonFrontendContribution as TheiaCommonFrontendContribution, CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
@injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
for (const command of [
CommonCommands.SAVE,
CommonCommands.SAVE_ALL,
CommonCommands.CUT,
CommonCommands.COPY,
CommonCommands.PASTE,
CommonCommands.COPY_PATH,
CommonCommands.FIND,
CommonCommands.REPLACE,
CommonCommands.AUTO_SAVE,
CommonCommands.OPEN_PREFERENCES,
CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME
]) {
registry.unregisterMenuAction(command);
}
}
}

View File

@@ -0,0 +1,55 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Disposable } from '@theia/core/lib/common/disposable';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus
} from '@theia/core/lib/browser/connection-status-service';
import { ArduinoDaemonClientImpl } from '../../arduino-daemon-client-impl';
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
@postConstruct()
protected init(): void {
this.schedulePing();
this.wsConnectionProvider.onIncomingMessageActivity(() => {
// natural activity
this.updateStatus(this.daemonClient.isRunning);
this.schedulePing();
});
}
}
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(ArduinoDaemonClientImpl)
protected readonly daemonClient: ArduinoDaemonClientImpl;
protected onStateChange(state: ConnectionStatus): void {
if (!this.daemonClient.isRunning && state === ConnectionStatus.ONLINE) {
return;
}
super.onStateChange(state);
}
protected handleOffline(): void {
const { isRunning } = this.daemonClient;
this.statusBar.setElement('connection-status', {
alignment: StatusBarAlignment.LEFT,
text: isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
tooltip: isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.',
priority: 5000
});
this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement('connection-status')));
document.body.classList.add('theia-mod-offline');
this.toDisposeOnOnline.push(Disposable.create(() => document.body.classList.remove('theia-mod-offline')));
}
}

View File

@@ -0,0 +1,31 @@
import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { CommandService } from '@theia/core/lib/common/command';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoCommands } from '../../arduino-commands';
@injectable()
export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(CommandService)
protected readonly commandService: CommandService;
protected async initializeLayout(): Promise<void> {
await super.initializeLayout();
const roots = await this.workspaceService.roots;
for (const root of roots) {
const exists = await this.fileSystem.exists(root.uri);
if (exists) {
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.uri);
}
}
}
}

View File

@@ -0,0 +1,28 @@
import { injectable } from 'inversify';
import { Command } from '@theia/core/lib/common/command';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { KeybindingRegistry as TheiaKeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding';
@injectable()
export class KeybindingRegistry extends TheiaKeybindingRegistry {
// https://github.com/eclipse-theia/theia/issues/8209
unregisterKeybinding(key: string): void;
unregisterKeybinding(keybinding: Keybinding): void;
unregisterKeybinding(command: Command): void;
unregisterKeybinding(arg: string | Keybinding | Command): void {
const keymap = this.keymaps[KeybindingScope.DEFAULT];
const filter = Command.is(arg)
? ({ command }: Keybinding) => command === arg.id
: ({ keybinding }: Keybinding) => Keybinding.is(arg)
? keybinding === arg.keybinding
: keybinding === arg;
for (const binding of keymap.filter(filter)) {
const idx = keymap.indexOf(binding);
if (idx !== -1) {
keymap.splice(idx, 1);
}
}
}
}

View File

@@ -1,9 +1,9 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { ShellLayoutRestorer as TheiaShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
@injectable()
export class ArduinoShellLayoutRestorer extends ShellLayoutRestorer {
export class ShellLayoutRestorer extends TheiaShellLayoutRestorer {
// Workaround for https://github.com/eclipse-theia/theia/issues/6579.
async storeLayoutAsync(app: FrontendApplication): Promise<void> {

View File

@@ -3,12 +3,12 @@ import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets';
import { ILogger } from '@theia/core';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../common/protocol/config-service';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../../common/protocol/config-service';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@inject(ConfigService)
protected readonly configService: ConfigService;
@@ -16,7 +16,6 @@ export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
@postConstruct()

View File

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

View File

@@ -1,8 +1,10 @@
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { injectable } from 'inversify';
import { EditorContribution as TheiaEditorContribution } 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 {
@injectable()
export class EditorContribution extends TheiaEditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {
}

View File

@@ -0,0 +1,37 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorManager as TheiaEditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { ConfigService } from '../../../common/protocol/config-service';
import { EditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
@injectable()
export class EditorManager extends TheiaEditorManager {
@inject(ConfigService)
protected readonly configService: ConfigService;
async open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const [widget, readOnly] = await Promise.all([super.open(uri, options), this.isReadOnly(uri)]);
if (readOnly) {
const { editor } = widget;
if (editor instanceof MonacoEditor) {
const codeEditor = editor.getControl();
codeEditor.updateOptions({ readOnly });
}
}
return widget;
}
protected async isReadOnly(uri: URI): Promise<boolean> {
const [config, configFileUri] = await Promise.all([
this.configService.getConfiguration(),
this.configService.getCliConfigFileUri()
]);
if (new URI(configFileUri).toString(true) === uri.toString(true)) {
return false;
}
return new URI(config.dataDirUri).isEqualOrParent(uri)
}
}

View File

@@ -0,0 +1,38 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SketchesService, Sketch } from '../../../common/protocol';
@injectable()
export class EditorWidgetFactory extends TheiaEditorWidgetFactory {
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected async createEditor(uri: URI): Promise<EditorWidget> {
const widget = await super.createEditor(uri);
return this.maybeUpdateCaption(widget);
}
protected async maybeUpdateCaption(widget: EditorWidget): Promise<EditorWidget> {
const sketch = await this.sketchesServiceClient.currentSketch();
const { uri } = widget.editor;
if (sketch && Sketch.isInSketch(uri, sketch)) {
const isTemp = await this.sketchesService.isTemp(sketch);
if (isTemp) {
widget.title.caption = `Unsaved ${this.labelProvider.getName(uri)}`;
}
}
return widget;
}
}

View File

@@ -2,11 +2,11 @@ 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';
import { ProblemContribution as TheiaProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class ArduinoProblemContribution extends ProblemContribution {
export class ProblemContribution extends TheiaProblemContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;

View File

@@ -3,11 +3,11 @@ 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';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../../common/protocol/config-service';
@injectable()
export class ArduinoProblemManager extends ProblemManager {
export class ProblemManager extends TheiaProblemManager {
@inject(ConfigService)
protected readonly configService: ConfigService;

View File

@@ -0,0 +1,368 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Copied from https://github.com/microsoft/vscode/blob/724c307bf35646ac549a8533a255c51b63fea5c7/src/vs/base/common/comparers.ts
// We cannot customize the monaco loader for Theia: https://github.com/eclipse-theia/theia/issues/8220
import { isWindows } from '@theia/core/lib/common/os';
const sep = (isWindows ? '\\' : '/');
interface IDisposable {
dispose(): void;
}
interface IdleDeadline {
readonly didTimeout: boolean;
timeRemaining(): number;
}
let runWhenIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
declare function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number;
declare function cancelIdleCallback(handle: number): void;
(function () {
if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') {
const dummyIdle: IdleDeadline = Object.freeze({
didTimeout: true,
timeRemaining() { return 15; }
});
runWhenIdle = (runner) => {
const handle = setTimeout(() => runner(dummyIdle));
let disposed = false;
return {
dispose() {
if (disposed) {
return;
}
disposed = true;
clearTimeout(handle);
}
};
};
} else {
runWhenIdle = (runner, timeout?) => {
const handle: number = requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined);
let disposed = false;
return {
dispose() {
if (disposed) {
return;
}
disposed = true;
cancelIdleCallback(handle);
}
};
};
}
})();
/**
* An implementation of the "idle-until-urgent"-strategy as introduced
* here: https://philipwalton.com/articles/idle-until-urgent/
*/
class IdleValue<T> {
private readonly _executor: () => void;
private readonly _handle: IDisposable;
private _didRun: boolean = false;
private _value?: T;
private _error: any;
constructor(executor: () => T) {
this._executor = () => {
try {
this._value = executor();
} catch (err) {
this._error = err;
} finally {
this._didRun = true;
}
};
this._handle = runWhenIdle(() => this._executor());
}
dispose(): void {
this._handle.dispose();
}
get value(): T {
if (!this._didRun) {
this._handle.dispose();
this._executor();
}
if (this._error) {
throw this._error;
}
return this._value!;
}
}
// When comparing large numbers of strings, such as in sorting large arrays, is better for
// performance to create an Intl.Collator object and use the function provided by its compare
// property than it is to use String.prototype.localeCompare()
// A collator with numeric sorting enabled, and no sensitivity to case or to accents
const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
return {
collator: collator,
collatorIsNumeric: collator.resolvedOptions().numeric
};
});
// A collator with numeric sorting enabled.
const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true });
return {
collator: collator
};
});
// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' });
return {
collator: collator
};
});
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
const a = one || '';
const b = other || '';
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);
// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
return a < b ? -1 : 1;
}
return result;
}
/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */
export function compareFileNamesNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
// Check for name differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
}
// Disambiguate the extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
}
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
other = other && other.toLowerCase();
}
const [oneName, oneExtension] = extractNameAndExtension(one);
const [otherName, otherExtension] = extractNameAndExtension(other);
if (oneName !== otherName) {
return oneName < otherName ? -1 : 1;
}
if (oneExtension === otherExtension) {
return 0;
}
return oneExtension < otherExtension ? -1 : 1;
}
export function compareFileExtensions(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one);
const [otherName, otherExtension] = extractNameAndExtension(other);
let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension);
if (result === 0) {
// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
return oneExtension < otherExtension ? -1 : 1;
}
// Extensions are equal, compare filenames
result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName);
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) {
return oneName < otherName ? -1 : 1;
}
}
return result;
}
/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */
export function compareFileExtensionsNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
// Check for extension differences, ignoring differences in case and comparing numbers numerically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
}
// Compare names.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Disambiguate extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
}
/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];
// if the dotfilesAsNames option is selected, treat an empty filename with an extension,
// or a filename that starts with a dot, as a dotfile name
if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) {
result = [result[0] + '.' + result[1], ''];
}
return result;
}
function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
// Check for differences
let result = collator.compare(one, other);
if (result !== 0) {
return result;
}
// In a numeric comparison, `foo1` and `foo01` will compare as equivalent.
// Disambiguate by sorting the shorter string first.
if (one.length !== other.length) {
return one.length < other.length ? -1 : 1;
}
return 0;
}
function comparePathComponents(one: string, other: string, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
other = other && other.toLowerCase();
}
if (one === other) {
return 0;
}
return one < other ? -1 : 1;
}
export function comparePaths(one: string, other: string, caseSensitive = false): number {
const oneParts = one.split(sep);
const otherParts = other.split(sep);
const lastOne = oneParts.length - 1;
const lastOther = otherParts.length - 1;
let endOne: boolean, endOther: boolean;
for (let i = 0; ; i++) {
endOne = lastOne === i;
endOther = lastOther === i;
if (endOne && endOther) {
return compareFileNames(oneParts[i], otherParts[i], caseSensitive);
} else if (endOne) {
return -1;
} else if (endOther) {
return 1;
}
const result = comparePathComponents(oneParts[i], otherParts[i], caseSensitive);
if (result !== 0) {
return result;
}
}
}
export function compareAnything(one: string, other: string, lookFor: string): number {
const elementAName = one.toLowerCase();
const elementBName = other.toLowerCase();
// Sort prefix matches over non prefix matches
const prefixCompare = compareByPrefix(one, other, lookFor);
if (prefixCompare) {
return prefixCompare;
}
// Sort suffix matches over non suffix matches
const elementASuffixMatch = elementAName.endsWith(lookFor);
const elementBSuffixMatch = elementBName.endsWith(lookFor);
if (elementASuffixMatch !== elementBSuffixMatch) {
return elementASuffixMatch ? -1 : 1;
}
// Understand file names
const r = compareFileNames(elementAName, elementBName);
if (r !== 0) {
return r;
}
// Compare by name
return elementAName.localeCompare(elementBName);
}
export function compareByPrefix(one: string, other: string, lookFor: string): number {
const elementAName = one.toLowerCase();
const elementBName = other.toLowerCase();
// Sort prefix matches over non prefix matches
const elementAPrefixMatch = elementAName.startsWith(lookFor);
const elementBPrefixMatch = elementBName.startsWith(lookFor);
if (elementAPrefixMatch !== elementBPrefixMatch) {
return elementAPrefixMatch ? -1 : 1;
}
// Same prefix: Sort shorter matches to the top to have those on top that match more precisely
// tslint:disable-next-line: one-line
else if (elementAPrefixMatch && elementBPrefixMatch) {
if (elementAName.length < elementBName.length) {
return -1;
}
if (elementAName.length > elementBName.length) {
return 1;
}
}
return 0;
}

View File

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

View File

@@ -0,0 +1,28 @@
import { injectable, inject } from 'inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class FileNavigatorContribution extends TheiaFileNavigatorContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
registerKeybindings(registry: KeybindingRegistry): void {
super.registerKeybindings(registry);
[
WorkspaceCommands.FILE_RENAME,
WorkspaceCommands.FILE_DELETE
].forEach(registry.unregisterKeybinding.bind(registry));
}
}

View File

@@ -1,10 +1,10 @@
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';
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
export class OutlineViewContribution extends TheiaOutlineViewContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;

View File

@@ -0,0 +1,15 @@
import { injectable } from 'inversify';
import { Message, Widget } from '@theia/core/lib/browser';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
// Patched after https://github.com/eclipse-theia/theia/issues/8361
// Remove this module after ATL-222 and the Theia update.
@injectable()
export class OutputWidget extends TheiaOutputWidget {
protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.onResize(Widget.ResizeMessage.UnknownSize);
}
}

View File

@@ -0,0 +1,25 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preferences-contribution';
@injectable()
export class PreferencesContribution extends TheiaPreferencesContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
// The settings group: preferences, CLI config is not part of the `File` menu on macOS.
// On Windows and Linux, we rebind it to `Preferences...`. It is safe to remove here.
registry.unregisterMenuAction(CommonCommands.OPEN_PREFERENCES.id, CommonMenus.FILE_SETTINGS_SUBMENU_OPEN);
}
registerKeybindings(registry: KeybindingRegistry): void {
// https://github.com/eclipse-theia/theia/issues/8202
registry.registerKeybinding({
command: CommonCommands.OPEN_PREFERENCES.id,
keybinding: 'CtrlCmd+,',
});
}
}

View File

@@ -1,10 +1,10 @@
import { inject, injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { EditorMode } from '../editor-mode';
import { EditorMode } from '../../editor-mode';
@injectable()
export class ArduinoScmContribution extends ScmContribution {
export class ScmContribution extends TheiaScmContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;

View File

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

View File

@@ -0,0 +1,139 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { open } from '@theia/core/lib/browser/opener-service';
import { FileStat } from '@theia/filesystem/lib/common';
import { CommandRegistry, CommandService } from '@theia/core/lib/common/command';
import { WorkspaceCommandContribution as TheiaWorkspaceCommandContribution, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { Sketch } from '../../../common/protocol';
import { WorkspaceInputDialog } from './workspace-input-dialog';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SingleTextInputDialog } from '@theia/core/lib/browser';
@injectable()
export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(CommandService)
protected readonly commandService: CommandService;
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.unregisterCommand(WorkspaceCommands.NEW_FILE);
registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({
execute: uri => this.newFile(uri)
}));
registry.unregisterCommand(WorkspaceCommands.FILE_RENAME);
registry.registerCommand(WorkspaceCommands.FILE_RENAME, this.newUriAwareCommandHandler({
execute: uri => this.renameFile(uri)
}));
}
protected async newFile(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
}
const parent = await this.getDirectory(uri);
if (!parent) {
return;
}
const parentUri = new URI(parent.uri);
const dialog = new WorkspaceInputDialog({
title: 'Name for new file',
parentUri,
validate: name => this.validateFileName(name, parent, true)
}, this.labelProvider);
const name = await dialog.open();
const nameWithExt = this.maybeAppendInoExt(name);
if (nameWithExt) {
const fileUri = parentUri.resolve(nameWithExt);
await this.fileSystem.createFile(fileUri.toString());
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
open(this.openerService, fileUri);
}
}
protected async validateFileName(name: string, parent: FileStat, recursive: boolean = false): Promise<string> {
// In the Java IDE the followings are the rules:
// - `name` without an extension should default to `name.ino`.
// - `name` with a single trailing `.` also defaults to `name.ino`.
const nameWithExt = this.maybeAppendInoExt(name);
const errorMessage = await super.validateFileName(nameWithExt, parent, recursive);
if (errorMessage) {
return errorMessage;
}
const extension = nameWithExt.split('.').pop();
if (!extension) {
return 'Invalid filename.'; // XXX: this should not happen as we forcefully append `.ino` if it's not there.
}
if (Sketch.Extensions.ALL.indexOf(`.${extension}`) === -1) {
return `.${extension} is not a valid extension.`;
}
return '';
}
protected maybeAppendInoExt(name: string | undefined): string {
if (!name) {
return '';
}
if (name.trim().length) {
if (name.indexOf('.') === -1) {
return `${name}.ino`
}
if (name.lastIndexOf('.') === name.length - 1) {
return `${name.slice(0, -1)}.ino`
}
}
return name;
}
protected async renameFile(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
}
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return;
}
if (uri.toString() === sketch.mainFileUri) {
const options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: true
};
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
return;
}
const parent = await this.getParent(uri);
if (!parent) {
return;
}
const initialValue = uri.path.base;
const dialog = new SingleTextInputDialog({
title: 'New name for file',
initialValue,
initialSelectionRange: {
start: 0,
end: uri.path.name.length
},
validate: (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
}
});
const newName = await dialog.open();
const newNameWithExt = this.maybeAppendInoExt(newName);
if (newNameWithExt) {
const oldUri = uri;
const newUri = uri.parent.resolve(newNameWithExt);
this.fileSystem.move(oldUri.toString(), newUri.toString());
}
}
}

View File

@@ -0,0 +1,36 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class WorkspaceDeleteHandler extends TheiaWorkspaceDeleteHandler {
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
async execute(uris: URI[]): Promise<void> {
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return;
}
// Deleting the main sketch file.
if (uris.map(uri => uri.toString()).some(uri => uri === sketch.mainFileUri)) {
const { response } = await remote.dialog.showMessageBox({
title: 'Delete',
type: 'question',
buttons: ['Cancel', 'OK'],
message: 'Do you want to delete the current sketch?'
});
if (response === 1) { // OK
await Promise.all([...sketch.additionalFileUris, ...sketch.otherSketchFileUris, sketch.mainFileUri].map(uri => this.closeWithoutSaving(new URI(uri))));
await this.fileSystem.delete(sketch.uri);
window.close();
}
return;
}
return super.execute(uris);
}
}

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