Compare commits

..

79 Commits

Author SHA1 Message Date
Alberto Iannaccone
4e882d25d9 bump arduino-fwuploader to 2.2.2 (#1584) 2022-10-27 14:53:36 +02:00
github-actions[bot]
f93f78039b Updated translation files (#1496)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-27 12:40:56 +02:00
Akos Kitta
2b2463b834 fix: Prompt sketch move when opening an invalid outside from IDE2
Log IDE2 version on start.

Closes #964
Closes #1484

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-26 18:53:00 +02:00
Nick B
0773c3915c Added an optional user modifiable default sketch file when creating a new project. (#1559)
* Added a modifiable default sketch for new project

* Removed unused file

* WiP : Now nothing's working... :(

* yarn i18n:generate for the settings

* Updated the desription for markdown description.

* Lintered the code

* Remove undesirable whitespaces

* Applied kittaakos suggestions

* Removed extra whitespaces

* Fixed default `.ino` for the missings empty lines.
2022-10-26 14:08:22 +02:00
Francesco Spissu
2f5afe0d9c Prevent layout shift on hover in libs/board manager (#1568) 2022-10-25 08:58:37 +02:00
Muhammad Zaheer
b8370686ec Coding style fix - newline added 2022-10-25 08:51:21 +02:00
Muhammad Zaheer
3b2d12eff9 Cleaner implementation of HistoryList
- The implementation has been taken from @kittaakos repo
d10de01736/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx
- The previous method has been modified to ensure the first element instead of an empty string is returned if the index is at the beginning of the list.
- The push method has been modified to check if the current command is same as the last command. If same then, it is not added to the list else it is added.
2022-10-25 08:51:21 +02:00
Muhammad Zaheer
cdaaa5584d Changed logic to avoid end value being shown twice 2022-10-25 08:51:21 +02:00
Muhammad Zaheer
3476de27f7 Added Message History to Serial Monitor 2022-10-25 08:51:21 +02:00
Akos Kitta
b55cfc2052 chore: Use 0.28.0 CLI in IDE2.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-24 16:08:57 +02:00
Akos Kitta
44751c370b Changed the daemon output from json to text
Closes #1544

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-24 14:14:33 +02:00
Alberto Iannaccone
32d904ca36 Let the user edit the font size settings with the keyboard (#1547)
* let the user edit the stepper input with keyboard

* consider exceptions and fix styling

* fix onBlur with empty strings

* always set the internal state value

* misc fixes

Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com>
2022-10-21 17:36:19 +02:00
Muhammad Zaheer
5424dfcf70 Fix #1566 : Port submenu section heading show at top 2022-10-21 09:04:07 +02:00
per1234
b8bf1eefa2 Allow uploads without port selection
It is common for a "port" to be used in some way during the process of uploading to a board. However, the array of
Arduino boards is very diverse. Some of these do not produce a port and their upload method has no need for one.

For this reason, the IDE must allow the upload process to be initiated regardless of whether a port happens to be
selected. During the addition of support for user provided fields, an unwarranted assumption was made that all boards
require a port selection for upload and this resulted in a regression that broke uploading for these boards. This
regression was especially user unfriendly in that there was no response whatsoever from the IDE when the user attempted
to initiate an upload under these conditions.

The bug is hereby fixed. The upload process will always be initiated by the IDE regardless of whether a port is
selected. In cases where a port is required, the resulting error message returned by Arduino CLI or the upload tool will
communicate the problem to the user.
2022-10-20 08:31:52 -07:00
Francesco Spissu
93291b6811 Adjust library installation dialog buttons style (#1401)
Closes #1314.
2022-10-20 12:40:40 +02:00
Akos Kitta
87ebcbe77e Let CSS do the uppercase transformation.
Expose no implementation details to translation files.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-19 10:49:02 +02:00
Akos Kitta
99b10942bb Listen on the client's port change event
If the board select dialog is listening on the backend's event,
the frontend might miss the event when it comes up, although boards
are connected and ports are discovered.

Closes #573

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-17 10:31:19 +02:00
Alberto Iannaccone
960a2d0634 Fix boards listing (#1520)
* Fix boards listing

* use arduio-cli sorting fix

* re-use code to handle board list response

* change `handleListBoards` visibility to `private`

* pad menu items order with leading zeros to fix alphanumeric order
2022-10-17 10:03:41 +02:00
Francesco Spissu
e577de4e8e Put Arduino libs and platforms on top of the Library/Boards Manager (#1541) 2022-10-14 09:07:54 +02:00
Francesco Spissu
f3ef95cfe2 Retain installation interface using version menu (#1471) 2022-10-13 12:05:29 +02:00
dankeboy36
bc264d1adf Apply margin adjustments to the first hover row
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2022-10-07 04:16:26 -07:00
dankeboy36
5444395f34 Better tooltips.
fixes #1503

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2022-10-07 04:16:26 -07:00
Akos Kitta
2d2be1f6d0 Ensure exact match when installing Arduino_BuiltIn
on the first IDE2 startup.

Closes #1526

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 11:55:53 +02:00
r3inbowari
1e269ac83d Fix status bar clipped in minimal state (#1517) 2022-10-07 10:43:45 +02:00
Akos Kitta
0c49709f26 Link resolved for lib/boards manager.
Closes #1442

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 10:00:36 +02:00
Akos Kitta
019b2d5588 Avoid using reportResult if installing lib/core
Closes #1529

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 09:01:25 +02:00
Alberto Iannaccone
aa0807ca3f Limit interface scale (#1502)
* limit interface scale

* debounce interface scale updates

* limit font-size + refactor

* remove excessive settings duplicate

* remove useless async

* fix interface scale step

* change mainMenuManager visibility to private

* fix menu registration

* update menu actions when autoScaleInterface changes
2022-10-06 17:37:26 +02:00
Akos Kitta
61a11a0857 Removed real_name of the libraries.
It has been removed from the gRPC API: arduino/arduino-cli#1890

This PR switches from `real_name` to `name` in the UI, as the `name` is
the canonical form provided by the CLI.

Closes #1525

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-05 13:47:38 +02:00
Akos Kitta
0c20ae0e28 Various library/platform index update fixes
- IDE2 can start if the package index download fails. Closes #1084
 - Split the lib and platform index update. Closes #1156

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-05 09:17:37 +02:00
per1234
945a8f4841 Bump built-in example sketches version to 1.10.0
The Arduino IDE installation includes a collection of example sketches demonstrating fundamental concepts.

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

This release updates the formatting of the examples to be compliant with the code style of the Arduino IDE 2.x
"Auto Format" feature.
2022-10-04 01:59:50 -07:00
per1234
ae76432944 Update library dependency installation dialog response indexes
Arduino libraries may specify dependencies on other libraries in their metadata. The installation of these dependencies
is offered by the Arduino IDE when the user installs the dependent library using the Library Manager widget.

The order of the buttons in the dialog that offers the dependencies installation was recently rearranged. The dialog
response interpretation code was not updated to reflect the changes to the button indexes at that time. This caused the
"CANCEL" button to trigger the behavior expected from the "INSTALL ALL" button, and vice versa.

The library dependencies installation dialog response interpretation code is hereby updated to use the new button
indexes.
2022-10-03 23:56:22 -07:00
per1234
40807db65e Bump arduino-serial-plotter-webapp dependency to 0.2.0 2022-10-03 23:33:17 -07:00
Akos Kitta
da22f1ed11 Refresh menus when opening example/recent fails.
Closes #53

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-04 08:10:27 +02:00
per1234
32b70efd5c Correct text of "INSTALLED" label in Library/Boards Manager
An "INSTALLED" label is shown on the items in the Library Manager and Boards Manager views that are currently installed
on the user's system.

During some work to add missing internationalization to the UI strings, this text was changed to "INSTALL". That text is
not appropriate for what this label is intended to communicate.

The regression is hereby corrected, while retaining the internationalization of the string.
2022-10-03 00:53:40 -07:00
Alberto Iannaccone
6f07717369 Dialog focus (#1472)
* focus on dialog controls when is open

* fix "Configure and Upload" label

* fix focus on user fields
2022-09-29 15:16:28 +02:00
r3inbowari
d6cb23f782 fix splitHandle above widget 2022-09-27 06:19:55 -07:00
r3inbowari
9ac2638335 Avoid intellisense widgets being covered by the bottom panel 2022-09-27 06:19:55 -07:00
Alberto Iannaccone
96cf09d594 Initialise the IDE updater even when 'checkForUpdates' preference is false (#1490) 2022-09-26 17:39:19 +02:00
per1234
8380c82028 Add readme for localization data
Arduino IDE has been translated to several languages.

The localization process follows the following steps:

1. An English language source string is defined in the Arduino IDE codebase
2. The source string is pushed to Transifex
3. Community translators localize the string
4. The localization data is pulled into the Arduino IDE repository
5. The localization data is incorporated into the Arduino IDE distribution

Experience with maintenance of Arduino's localized projects indicates that the data files generated at step (4) can
appear to be the appropriate place to make edits for casual contributors not familiar with the project's sophisticated
internationalization infrastructure.

Since those files are generated by automated systems, any edits made there would only be overwritten, so it is important
to clearly communicate the correct way to make enhancements or corrections to these strings. This is accomplished by a
local readme file most likely to be seen by those working in the folder containing these files, which supplements the
existing information about translation in the project's translation guide.
2022-09-26 05:07:34 -07:00
per1234
5eb2926407 Add a dedicated translator guide document
Translation of the strings of the Arduino IDE UI is a valuable contribution which helps to make Arduino accessible to
everyone around the world.

Localization of the Arduino-specific strings of the IDE is done in the "Arduino IDE 2.0" project on Transifex.
Previously, the "Translation" row in the contribution methods summary table in the contributor guide entry page simply
linked to that project.

Arduino IDE also uses localized strings from several other sources:

- VS Code language packs
- Arduino CLI

Users may notice unlocalized strings or errors or areas for improvement in the existing translations and wish to
contribute translations. For this reason, it is important to also provide instructions for contributing to those other
localization data sources. The contribution methods summary table can not effectively accommodate that additional
content so a dedicated document is added for the purpose. This will also allow linking directly to that document from
related documentation or conversations.
2022-09-26 05:07:34 -07:00
per1234
a4ab204400 Correct issue report guide link in issue template chooser
Contributor are presented with an issue template chooser page at the start of the issue creation process.

In addition to the issue report templates, some "contact links" provide information and links to other communication
channels. In order to encourage high quality issues, a link to the "issue report guide" is included on this page.

Previously that link pointed to an incorrect URL, resulting in a 404 error for those who visited it. The URL is hereby
corrected.
2022-09-26 05:04:43 -07:00
per1234
6416c431c6 Bump version metadata to produce correct tester and nightly build precedence
On every startup, the Arduino IDE checks for new versions of the IDE. If a newer version is available, a
notification/dialog is shown offering an update.

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

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

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

The previous release procedure caused the <version> component of these to be the version of the most recent release.

During the pre-release phase of the project development, all releases had a pre-release suffix (e.g., 2.0.0-rc9.4).
Appending the "snapshot" or "nightly" suffix to that pre-release version caused these builds to have the correct
precedence (e.g., 2.0.0-rc9.2.snapshot-20cc34c > 2.0.0-rc9.2). This situation has changed now that the project is using
production release versions (e.g., 2.0.0-nightly-20220915 < 2.0.0). This caused users of "snapshot" or "nightly" builds
to be presented with a spurious update notification on startup.

The solution is to do a minor bump of the version metadata after creating the release tag. That was not done immediately
following the 2.0.0 release. The omission is hereby corrected.

This will provide the metadata bump traditionally done before the creation of the release tag in the event the version
number of the next release is 2.0.1. In case it is instead a minor or major release, the version metadata will need to
be updated once more before the release tag is created.
2022-09-26 05:04:43 -07:00
per1234
8f88aa69bf Adjust release procedure to produce correct tester and nightly build version precedence
On every startup, the Arduino IDE checks for new versions of the IDE. If a newer version is available, a
notification/dialog is shown offering an update.

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

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

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

The previous release procedure caused the <version> component of these to be the version of the most recent release.

During the pre-release phase of the project development, all releases had a pre-release suffix (e.g., 2.0.0-rc9.4).
Appending the "snapshot" or "nightly" suffix to that pre-release version caused these builds to have the correct
precedence (e.g., 2.0.0-rc9.2.snapshot-20cc34c > 2.0.0-rc9.2). This situation has changed now that the project is using
production release versions (e.g., 2.0.0-nightly-20220915 < 2.0.0). This caused users of "snapshot" or "nightly" builds
to be presented with a spurious update notification on startup.

The solution is to add a step to the end of the release procedure to do a minor bump of the version metadata after
creating the release tag.

This means that the metadata bump traditionally done before the creation of the release tag will already have been done
in advance for patch releases. However, it will still need to be done for minor or major releases.

The release procedure documentation is hereby updated to produce correct tester and nightly build version precedence.

The metadata bump step is moved from before to after the tag creation step, replaced by a new step to verify the version
before the tag creation, updating it in the event it is not a patch release. Both those steps may require updating the
metadata. As an alternative to maintaining duplicate copies, each step links to a single copy of the fairly complex
instructions for doing so. The structure of the document is adjusted to accomodate this, by placing the steps of the
procedure under a "Steps" section and creating a new "Operations" section to contain any such shared content.
2022-09-26 05:04:43 -07:00
per1234
3c2b2a0734 Format release procedure document as ordered list
The release procedure is a set of steps which must be performed in a specific sequence. This fact is more effectively
communicated by formatting it as an ordered list.
2022-09-26 05:04:43 -07:00
per1234
39538f163f Move package metadata update step to dedicated section of release docs
Previously the instructions for updating the npm package metadata, submitting a PR for that, and merging the PR was in
the same section as the tag push instructions in the release procedure documentation.

These two operations are distinct from each other. Mashing them into a single step makes the release procedure document
difficult to read and the process more prone to error.

For this reason, a dedicated step is used for each of the two things.
2022-09-26 05:04:43 -07:00
Akos Kitta
9ef04bb8d6 Fixed missing translations
Aligned the languge pack versions.

Closes #1431

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-26 10:49:31 +02:00
Akos Kitta
707f3bef61 Listen on keyboard layout changes from the OS.
Closes #989

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-26 10:49:03 +02:00
Akos Kitta
878395221a Use the parent of the existing sketch if not temp.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-22 10:08:38 +02:00
Akos Kitta
6a35bbfa7e Made the file dialogs modal.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-22 10:08:38 +02:00
Francesco Spissu
42f6f43870 Avoid new line if 3rd party URLs text is too long (#1474)
Closes #1470.
2022-09-21 11:51:38 +02:00
Akos Kitta
6983c5bf7f Ensure directories.user exists.
Closes #1445

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-21 09:10:26 +02:00
Francesco Spissu
b3ab5cbd2a Fix input background in Firmware Updater dialog (#1465)
Closes #1441.
2022-09-20 14:47:30 +02:00
Alberto Iannaccone
8a5995920a fix board selection and workspace input dialogs width and height (#1406)
* fix board selection and workspace input dialogs width and height

* use same dialog for new file and rename

* fix board list getting small when filtering

* board select dialog: show variant text when no board is found

* fix addition boards url outline
2022-09-20 14:36:02 +02:00
github-actions[bot]
8de6cf84d9 Updated translation files (#1462)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-20 12:57:27 +02:00
Dwight
f5c36bb691 Serial Monitor autoscroll only makes bottom line partially visible #972 (#1446) 2022-09-20 12:26:57 +02:00
Francesco Spissu
364f8b8e51 Move primary buttons on the right of the dialogs (#1382)
Closes #1368.
2022-09-20 11:48:19 +02:00
Alberto Iannaccone
671d2eabd4 Show user fields dialog again if upload fails (#1415)
* Show user fields dialog again if upload fails

* move user fields logic into own contribution

* apply suggestions
2022-09-20 09:27:09 +02:00
Akos Kitta
9a65ef6ea8 Disabled the tokenizer after 500 chars.
Closes #1343.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-19 12:17:13 +02:00
Alberto Iannaccone
4e590ab618 use ipc message to focus on serial plotter window (#1410) 2022-09-15 16:07:13 +02:00
Dave Simpson
026e80e7fc fix #1383: missing port labels (#1412)
* fallback to port.address if addressLabel is false

* addressLabel if possible, reconcileAvailableBoards

* remove fallback
2022-09-15 15:29:28 +02:00
Akos Kitta
fdf6f0f9c8 Avoid deleting the workspace when it's still in use.
- From now on, NSFW service disposes after last reference
is removed. No more 10sec delay.
 - Moved the temp workspace deletion to a startup task.
 - Can set initial task for the window from electron-main.
 - Removed the `browser-app`.

Closes #39

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-15 15:24:25 +02:00
per1234
0151e4c224 Make instructions re: non-notarized PR tester build more user friendly
For security reasons, the macOS tester builds generated for PRs from forks are not notarized. Instructions are provided for
beta testing under these conditions.

Previously, the instructions for bypassing the macOS notarization requirement involved disabling macOS Gatekeeper
entirely using the spctl command. These instructions are hereby replaced by an alternative approach, where the
restriction is bypassed for the Arduino IDE application alone, via the macOS GUI.

The new approach is superior for the following reasons:

- More secure due to having limited scope
- More accessible due to the use of the macOS GUI
2022-09-15 05:53:09 -07:00
per1234
e8b0ea4f2d Reduce overlap between readme and development+contributor guides
The readme should provide a concise overview of the essential information about the project. Additional details are
provided in dedicated documents, so the readme only needs to provide links to that information.
2022-09-15 05:53:09 -07:00
per1234
7c1ca04c75 Add a project contributor guide
Documentation of how to contribute to the project gives everyone the opportunity to participate, while also reducing the
maintenance effort and increasing the quality of contributions.

This guide documents the various ways of contributing to the project.
2022-09-15 05:53:09 -07:00
per1234
0ba88d5ab6 Move beta testing information to a dedicated documentation file
Previously, the information about tester builds was mixed in with the documentation about building the project from
source.

The two subjects are of relevance to two distinct contribution options. Building from source will primarily be done by
developers working on the project code base. Tester builds will be used by beta testers and reviewers.

For this reason, it doesn't make sense to require beta testers to wade through a lot of development documentation not
directly related to their work in order to find the instructions for obtaining tester builds. Likewise, it doesn't make
sense to clutter up the development documentation with such information.

Moving the information about tester builds to a dedicated file makes it easier for the interested parties to find, and
also allows the creation of a comprehensive guide for beta testers without making a negative impact on the development
documentation content.
2022-09-15 05:53:09 -07:00
per1234
96e229d803 Move documentation assets to standard location 2022-09-15 05:53:09 -07:00
per1234
d07d83fdfe Move development documentation to a more suitable location
Previously, information about project development was stored in a file named BUILDING.md in the root of the repository.

That content is hereby moved to the file docs/development.md. This will provide the following benefits:

- Broaden the scope of the file to all information related to development rather than building only
- Store all documentation content under a single folder instead of randomly putting some in the root and others under
  docs
2022-09-15 05:53:09 -07:00
Akos Kitta
5f82577bc1 Can send message to the monitor with Enter.
Removed the required `Ctrl/Cmd` modifier.

Closes #572

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-14 17:18:31 +02:00
per1234
35fcfb89c1 Remove obsolete fragment identifier from IDE manual download link
The Arduino IDE has an auto update capability. A new version is checked for on every startup, and if available the user
is offered an update.

Although this update can usually be done automatically by the IDE, under some conditions this is not possible. For
example:

- The IDE package in use does not support auto update (i.e., Linux ZIP package)
- The file could not be downloaded due to a transient network service outage

In this case, the user is presented with a friendly dialog that explains the situation and links to the "Software" page
on arduino.cc, where the user can manually download and install the new version.

During the pre-release development phase of the project, the download links for Arduino IDE 2.x were on a sub-section of
the "Software" page. For this reason, the linked URL included the fragment identifier `#experimental-software` so that
the page would load scrolled down to the anchor at that section of the page.

With the 2.0.0 release, the Arduino IDE 2.x project has graduated to a production development phase. For this reason,
the download links have been moved to the top of the "Software" page and the now inaccurate `experimental-software`
anchor removed from the page.

The previous link with that fragment is still perfectly functional, but the fragment to a non-existent anchor serves no
purpose and also miscommunicates the project status to users who notice the URL that was loaded. For this reason, it is
hereby removed from the link.
2022-09-14 05:14:58 -07:00
Alberto Iannaccone
6e3fe08c4c update README.md 2022-09-13 23:58:23 -07:00
Alberto Iannaccone
7f06b148f4 Revert "change naming of nightly and snapshot builds (#1326)"
This reverts commit 5be1f9d7fe.
2022-09-13 23:58:23 -07:00
Alberto Iannaccone
bf303d1b2f 2.0.0 2022-09-13 23:58:23 -07:00
per1234
59ca91d805 Remove table of nightly build links from readme
During the early phase of development, the download links for the nightly build were only available from the table in the project readme.

Since that time, download links were also added to the "Software" page on arduino.cc, which is already linked to from
the readme. This means the nightly build link table is superfluous and only harms the readability of the readme.

The superfluous table is hereby removed from the readme.
2022-09-12 06:04:36 -07:00
github-actions[bot]
69bb0aa385 Updated translation files (#1421)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-12 09:53:06 +02:00
per1234
565970e779 Update release procedure documentation
An internal release procedure document was created separately from the documentation hosted in this repository. That
internal document became significantly more comprehensive and up to date than the unmaintained documentation in this
repository.

In order to avoid either the burden of maintaining two copies of the same information, or more likely the out of sync
state between the information in the two resulting from lack of such maintenance, a single document will be maintained
here in this repository.

The superior version of the information from the internal document is hereby migrated to the repository where it will be
maintained from here on.
2022-09-08 13:30:36 -07:00
per1234
fec3b1138b Move release procedure documentation to more appropriate location
Previously, the procedure for creating a new release of the project was included in the development documentation.

This information is distinct from the rest of the contents of that file in that it is not of any value or interest to
most contributors from the community since only project maintainers will ever create a release. This meant that it make
the document less readable and approachable without adding significant value in return.

The information is still essential to the project maintainers, so it must not be removed, but it can be moved to a
dedicated file under the existing `docs/internal/` folder that is specifically intended for storing such information.
2022-09-08 13:30:36 -07:00
Alberto Iannaccone
dcc0c0aa5d Prepare 2.0.0-rc9.4 (#1411) 2022-09-08 10:00:42 +02:00
Muhammad Zaheer
76673cb553 Time format in SerialMonitor changed.Fixes #580 2022-09-08 00:15:26 -07:00
per1234
8f95fd6ca6 Add instructions for accessing IDE's advanced settings
Although the Arduino IDE's primary preferences interface provides all required configuration capabilities, advanced
users may wish to fine tune the behavior of the application or temporarily enable additional log output to use for
troubleshooting problems with the IDE.

The IDE provides such settings in a separate interface.

Previously, the existence and access procedure for these settings was undocumented.

Since this is an advanced capability, the documentation is not appropriate for inclusion with the standard user
documentation on arduino.cc. A file here in the Arduino IDE is used instead. This file will serve as a container for all
such user-targeted documentation.
2022-09-07 00:16:14 -07:00
182 changed files with 8426 additions and 3477 deletions

View File

@@ -17,7 +17,6 @@ module.exports = {
'scripts/*',
'electron/*',
'electron-app/*',
'browser-app/*',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
],

View File

@@ -8,6 +8,12 @@ contact_links:
- name: Support request
url: https://forum.arduino.cc/
about: We can help you out on the Arduino Forum!
- name: Issue report guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
about: Learn about submitting issue reports to this repository.
- name: Contributor guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
about: Learn about contributing to this project.
- name: Discuss development work on the project
url: https://groups.google.com/a/arduino.cc/g/developers
about: Arduino Developers Mailing List

31
.vscode/launch.json vendored
View File

@@ -80,37 +80,6 @@
"port": 9222,
"webRoot": "${workspaceFolder}/electron-app"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--plugins=local-dir:plugins"
],
"windows": {
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",

30
.vscode/tasks.json vendored
View File

@@ -12,17 +12,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": true
}
},
{
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
@@ -34,17 +23,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
@@ -56,14 +34,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Browser App"
]
},
{
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",

View File

@@ -1,152 +1,3 @@
# Development
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
## Architecture overview
The 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 one 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 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 IDE, only the _backend_ allows OS interaction.
The _backend_ process is responsible for:
- providing access to the filesystem,
- communicating with the [Arduino CLI](https://github.com/arduino/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
## 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 IDE locally.
Please refer to the [Theia IDE prerequisites](https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites) documentation for the setup instructions.
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
Once you have all the tools installed, you can build the editor following these steps
1. Install the dependencies and build
```sh
yarn
```
2. Rebuild the dependencies
```sh
yarn rebuild:browser
```
3. Rebuild the electron dependencies
```sh
yarn rebuild:electron
```
4. Start the application
```sh
yarn start
```
### Notes for Windows contributors
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
### CI
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts for each operating system can be downloaded from the GitHub Actions page.
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
- Create a local tag:
```sh
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
```
## Notes for macOS contributors
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
Due to this limitation, Mac users have two options for testing contributions from forks:
### The Safe approach (recommended)
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
### The Risky approach
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
### Creating a release
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
- Make sure the `main` state represents what you want to release and you're on `main`.
- Prepare a release-candidate build on a branch:
```bash
git branch 0.2.0-rc \
&& git checkout 0.2.0-rc
```
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
```bash
yarn update:version 0.2.0
```
- This should generate multiple outgoing changes with the version update.
- Commit your changes and push to the remote:
```bash
git add . \
&& git commit -s -m "Updated versions to 0.2.0" \
&& git push
```
- Create the GH PR the workflow starts automatically.
- Once you're happy with the RC, merge the changes to the `main`.
- Create a tag and push it:
```bash
git tag -a 0.2.0 -m "0.2.0" \
&& git push origin 0.2.0
```
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
- If you do not like the generated changelog, modify it and update the GH release.
## FAQ
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
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.
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
[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 IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
# Development Guide
This documentation has been moved [**here**](docs/development.md#development-guide).

View File

@@ -1,45 +1,18 @@
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
# Arduino IDE 2.x (beta)
# Arduino IDE 2.x
[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
> ⚠️ This is **beta** software. Help us test it!
![](static/screenshot.png)
## Download
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
### Nightly builds
These builds are generated every day at 03:00 GMT from the `main` branch and
should be considered unstable:
| Platform | 32 bit | 64 bit |
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
| macOS | | [Nightly macOS 64 bit] |
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, redirecting to latest
> generated builds by replacing `latest` with the latest available build
> date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
> replaced with `20190806`)
You can download the latest release version and nightly builds from the [software download page on the Arduino website](https://www.arduino.cc/en/software).
## Support
@@ -47,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
## Bugs & Issues
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository.
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
### Security
@@ -62,16 +34,15 @@ e-mail contact: security@arduino.cc
## Contributions and development
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
Contributions are very welcome! There are several ways to participate in this project, including:
This repository contains the main code, but two more repositories are included during the build process:
- Fixing bugs
- Beta testing
- Translation
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
## Donations

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc9.3",
"version": "2.0.1",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -45,6 +45,7 @@
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/is-valid-path": "^0.1.0",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
"@types/lodash.debounce": "^4.0.6",
@@ -56,7 +57,7 @@
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.1.0",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
@@ -147,11 +148,9 @@
"frontend": "lib/browser/arduino-ide-frontend-module"
},
{
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontend": "lib/browser/theia/core/browser-window-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
@@ -160,10 +159,10 @@
],
"arduino": {
"cli": {
"version": "0.27.0-rc.1"
"version": "0.28.0"
},
"fwuploader": {
"version": "2.2.0"
"version": "2.2.2"
},
"clangd": {
"version": "14.0.0"

View File

@@ -1,7 +1,7 @@
// @ts-check
// The version to use.
const version = '1.9.1';
const version = '1.10.0';
(async () => {
const os = require('os');

View File

@@ -53,8 +53,6 @@ import {
DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import {
@@ -105,7 +103,8 @@ import {
} from '@theia/core/lib/browser/connection-status-service';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import {
FileSystemExt,
FileSystemExtPath,
@@ -206,12 +205,8 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
import {
MonacoEditorFactory,
@@ -308,7 +303,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './widgets/sketchbook/startup-task';
import { StartupTasks } from './contributions/startup-task';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer';
@@ -334,6 +329,12 @@ import {
} from './widgets/component-list/filter-renderer';
import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
import { UserFields } from './contributions/user-fields';
import { UpdateIndexes } from './contributions/update-indexes';
import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
@@ -398,6 +399,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
LibraryListWidgetFrontendContribution
);
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
// Sketch list service
bind(SketchesService)
@@ -433,6 +435,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
bind(CommandContribution).toService(BoardsServiceProvider);
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution)
@@ -463,6 +466,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
BoardsListWidgetFrontendContribution
);
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -600,9 +604,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoEditorProvider).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).toService(EditorManager);
@@ -612,17 +613,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(SearchInWorkspaceFactory)
.inSingletonScope();
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
({ container }) => {
const childContainer = createTreeContainer(container);
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
childContainer
.rebind(TreeWidget)
.toService(SearchInWorkspaceResultTreeWidget);
return childContainer.get(SearchInWorkspaceResultTreeWidget);
}
);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
rebind(TheiaApplicationConnectionStatusContribution).toService(
@@ -757,6 +747,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch);
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

View File

@@ -249,6 +249,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: true,
},
'arduino.sketch.inoBlueprint': {
type: 'string',
markdownDescription: nls.localize(
'arduino/preferences/sketch/inoBlueprint',
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
),
default: undefined,
},
},
};
@@ -278,6 +286,7 @@ export interface ArduinoConfiguration {
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean;
}

View File

@@ -34,6 +34,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());

View File

@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
AttachedBoardsChangeEvent,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
@@ -113,11 +112,14 @@ export class BoardsConfig extends React.Component<
);
}
}),
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
this.updatePorts(
event.newState.ports,
AttachedBoardsChangeEvent.diff(event).detached.ports
)
this.props.boardsServiceProvider.onAvailablePortsChanged(
({ newState, oldState }) => {
const removedPorts = oldState.filter(
(oldPort) =>
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
);
this.updatePorts(newState, removedPorts);
}
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
@@ -132,7 +134,7 @@ export class BoardsConfig extends React.Component<
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexDidUpdate(() =>
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
@@ -259,9 +261,12 @@ export class BoardsConfig extends React.Component<
override render(): React.ReactNode {
return (
<>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
@@ -299,6 +304,18 @@ export class BoardsConfig extends React.Component<
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
@@ -315,19 +332,17 @@ export class BoardsConfig extends React.Component<
/>
<i className="fa fa-search"></i>
</div>
<div className="boards list">
{Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
))}
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
@@ -342,7 +357,7 @@ export class BoardsConfig extends React.Component<
);
}
return !ports.length ? (
<div className="loading noselect">
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
@@ -374,7 +389,9 @@ export class BoardsConfig extends React.Component<
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
@@ -413,53 +430,5 @@ export namespace BoardsConfig {
const { name } = selectedBoard;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
}
}

View File

@@ -111,7 +111,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, {
commandId,
order: `${i}`,
order: String(i).padStart(4),
label,
});
return Disposable.create(() =>

View File

@@ -1,7 +1,12 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandService } from '@theia/core/lib/common/command';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
@@ -23,9 +28,18 @@ import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Unknown } from '../../common/nls';
import {
StartupTask,
StartupTaskProvider,
} from '../../electron-common/startup-task';
@injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution {
export class BoardsServiceProvider
implements
FrontendApplicationContribution,
StartupTaskProvider,
CommandContribution
{
@inject(ILogger)
protected logger: ILogger;
@@ -49,7 +63,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
newState: Port[];
oldState: Port[];
}>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -105,8 +123,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state);
this._attachedBoards = attachedBoards;
const oldState = this._availablePorts.slice();
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
await this.reconcileAvailableBoards();
@@ -115,6 +137,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(USE_INHERITED_CONFIG, {
execute: (inheritedConfig: BoardsConfig.Config) =>
this.inheritedConfig.resolve(inheritedConfig),
});
}
get reconciled(): Promise<void> {
return this._reconciled.promise;
}
@@ -207,8 +236,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
this._attachedBoards = event.newState.boards;
const oldState = this._availablePorts.slice();
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an
@@ -376,14 +409,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
if (!this._boardsConfig.selectedBoard) {
return [];
}
const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) {
return [];
}
const protocol = this._boardsConfig.selectedPort.protocol;
// Protocol must be set to `default` when uploading without a port selected:
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
}
@@ -578,6 +613,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
boardsConfig.selectedBoard &&
availableBoards.every(({ selected }) => !selected)
) {
let port = boardsConfig.selectedPort;
// If the selected board has the same port of an unknown board
// that is already in availableBoards we might get a duplicate port.
// So we remove the one already in the array and add the selected one.
@@ -585,11 +621,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
(board) => board.port?.address === boardsConfig.selectedPort?.address
);
if (found >= 0) {
// get the "Unknown board port" that we will substitute,
// then we can include it in the "availableBoard object"
// pushed below; to ensure addressLabel is included
port = availableBoards[found].port;
availableBoards.splice(found, 1);
}
availableBoards.push({
...boardsConfig.selectedBoard,
port: boardsConfig.selectedPort,
port,
selected: true,
state: AvailableBoard.State.incomplete,
});
@@ -655,11 +695,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
let storedLatestBoardsConfig = await this.getData<
BoardsConfig.Config | undefined
>('latest-boards-config');
// Try to get from the URL if it was not persisted.
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
if (!storedLatestBoardsConfig) {
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
new URL(window.location.href)
);
storedLatestBoardsConfig = await Promise.race([
this.inheritedConfig.promise,
new Promise<undefined>((resolve) =>
setTimeout(() => resolve(undefined), 2_000)
),
]);
}
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
@@ -682,8 +725,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
key
);
}
tasks(): StartupTask[] {
return [
{
command: USE_INHERITED_CONFIG.id,
args: [this.boardsConfig],
},
];
}
}
/**
* It should be neither visible nor called from outside.
*
* This service creates a startup task with the current board config and
* passes the task to the electron-main process so that the new window
* can inherit the boards config state of this service.
*
* Note that the state is always set, but new windows might ignore it.
* For example, the new window already has a valid boards config persisted to the local storage.
*/
const USE_INHERITED_CONFIG: Command = {
id: 'arduino-use-inherited-boards-config',
};
/**
* 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`.

View File

@@ -1,10 +1,11 @@
import { injectable } from '@theia/core/shared/inversify';
import { BoardsListWidget } from './boards-list-widget';
import type {
import {
BoardSearch,
BoardsPackage,
} from '../../common/protocol/boards-service';
import { URI } from '../contributions/contribution';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { BoardsListWidget } from './boards-list-widget';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
@@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
});
}
override async initializeLayout(): Promise<void> {
this.openView();
protected canParse(uri: URI): boolean {
try {
BoardSearch.UriParser.parse(uri);
return true;
} catch {
return false;
}
}
protected parse(uri: URI): BoardSearch | undefined {
return BoardSearch.UriParser.parse(uri);
}
}

View File

@@ -15,7 +15,7 @@ import { CurrentSketch } from '../../common/protocol/sketches-service-client-imp
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
private readonly fileDialogService: FileDialogService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
@@ -31,7 +31,7 @@ export class AddFile extends SketchContribution {
});
}
protected async addFile(): Promise<void> {
private async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
@@ -41,6 +41,7 @@ export class AddFile extends SketchContribution {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
modal: true,
});
if (!toAddUri) {
return;

View File

@@ -17,13 +17,13 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
private readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
private readonly responseService: ResponseServiceClient;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
private readonly libraryService: LibraryService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
@@ -43,23 +43,26 @@ export class AddZipLibrary extends SketchContribution {
});
}
async addZipLibrary(): Promise<void> {
private async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
),
defaultPath,
properties: ['openFile'],
filters: [
{
name: nls.localize('arduino/library/zipLibrary', 'Library'),
extensions: ['zip'],
},
],
});
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
),
defaultPath,
properties: ['openFile'],
filters: [
{
name: nls.localize('arduino/library/zipLibrary', 'Library'),
extensions: ['zip'],
},
],
}
);
if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try {

View File

@@ -28,7 +28,7 @@ export class ArchiveSketch extends SketchContribution {
});
}
protected async archiveSketch(): Promise<void> {
private async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
@@ -43,13 +43,16 @@ export class ArchiveSketch extends SketchContribution {
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
});
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath || canceled) {
return;
}

View File

@@ -5,7 +5,6 @@ import {
DisposableCollection,
Disposable,
} from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget';
@@ -200,14 +199,15 @@ PID: ${PID}`;
});
// Installed boards
for (const board of installedBoards) {
installedBoards.forEach((board, index) => {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel =
packageName +
`${manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
`${
manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
}`;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
@@ -240,14 +240,18 @@ PID: ${PID}`;
};
// Board menu
const menuAction = { commandId: id, label: name };
const menuAction = {
commandId: id,
label: name,
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
}
});
// Installed ports
const registerPorts = (
@@ -267,8 +271,12 @@ PID: ${PID}`;
];
const placeholder = new PlaceholderMenuNode(
menuPath,
`${firstToUpperCase(protocol)} ports`,
{ order: protocolOrder.toString() }
nls.localize(
'arduino/board/typeOfPorts',
'{0} ports',
Port.Protocols.protocolLabel(protocol)
),
{ order: protocolOrder.toString().padStart(4) }
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
@@ -279,11 +287,13 @@ PID: ${PID}`;
// First we show addresses with recognized boards connected,
// then all the rest.
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
});
const sortedIDs = Object.keys(ports).sort(
(left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
}
);
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
@@ -319,7 +329,7 @@ PID: ${PID}`;
const menuAction = {
commandId: id,
label,
order: `${protocolOrder + i + 1}`,
order: String(protocolOrder + i + 1).padStart(4),
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
@@ -351,7 +361,7 @@ PID: ${PID}`;
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.searchBoards({});
const allBoards = await this.boardsService.getInstalledBoards();
return allBoards.filter(InstalledBoardWithPackage.is);
}
}

View File

@@ -37,16 +37,17 @@ export class CheckForIDEUpdates extends Contribution {
}
override onReady(): void {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
return;
}
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(() => {
if (!this.preferences['arduino.checkForUpdates']) {
return;
}
return this.updater.checkForUpdates(true);
})
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>(

View File

@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
@@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
export {
Command,

View File

@@ -0,0 +1,45 @@
import { injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
SketchContribution,
Sketch,
} from './contribution';
@injectable()
export class DeleteSketch extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (uri: string) => this.deleteSketch(uri),
});
}
private async deleteSketch(uri: string): Promise<void> {
const sketch = await this.loadSketch(uri);
if (!sketch) {
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
return;
}
return this.sketchService.deleteSketch(sketch);
}
private async loadSketch(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
return undefined;
}
throw err;
}
}
}
export namespace DeleteSketch {
export namespace Commands {
export const DELETE_SKETCH: Command = {
id: 'arduino-delete-sketch',
};
}
}

View File

@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
/* Tools */ registry.registerCommand(
EditContributions.Commands.AUTO_FORMAT,
{ execute: () => this.run('editor.action.formatDocument') }
@@ -147,23 +123,6 @@ ${value}
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: nls.localize('vscode/findController/startFindAction', 'Find'),
@@ -220,15 +179,6 @@ ${value}
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',
@@ -315,12 +265,6 @@ export namespace EditContributions {
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

@@ -21,16 +21,23 @@ import {
MenuModelRegistry,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import {
Board,
SketchRef,
SketchContainer,
SketchesError,
Sketch,
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
export abstract class Examples extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@@ -38,6 +45,9 @@ export abstract class Examples extends SketchContribution {
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
protected abstract update(options?: {
board?: Board | undefined;
forceRefresh?: boolean;
}): void;
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
@@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
const sketch = await this.clone(uri);
if (sketch) {
try {
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// Do not toast the error message. It's handled by the `Open Sketch` command.
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
},
};
}
private async clone(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.cloneExample(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
}
@injectable()
export class BuiltInExamples extends Examples {
override async onReady(): Promise<void> {
this.register(); // no `await`
this.update(); // no `await`
}
protected async register(): Promise<void> {
protected override async update(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined;
try {
sketchContainers = await this.examplesService.builtIns();
@@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void {
this.notificationCenter.onLibraryDidInstall(() => this.register());
this.notificationCenter.onLibraryDidUninstall(() => this.register());
this.notificationCenter.onLibraryDidInstall(() => this.update());
this.notificationCenter.onLibraryDidUninstall(() => this.update());
}
override async onReady(): Promise<void> {
this.register(); // no `await`
this.update(); // no `await`
}
protected override handleBoardChanged(board: Board | undefined): void {
this.register(board);
this.update({ board });
}
protected async register(
board: Board | undefined = this.boardsServiceClient.boardsConfig
.selectedBoard
protected override async update(
options: { board?: Board; forceRefresh?: boolean } = {
board: this.boardsServiceClient.boardsConfig.selectedBoard,
}
): Promise<void> {
const { board, forceRefresh } = options;
return this.queue.add(async () => {
this.toDispose.dispose();
if (forceRefresh) {
await this.coreService.refresh();
}
const fqbn = board?.fqbn;
const name = board?.name;
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.

View File

@@ -7,6 +7,8 @@ import {
} from '../../common/protocol';
import { Contribution } from './contribution';
const Arduino_BuiltIn = 'Arduino_BuiltIn';
@injectable()
export class FirstStartupInstaller extends Contribution {
@inject(LocalStorageService)
@@ -25,8 +27,8 @@ export class FirstStartupInstaller extends Contribution {
id: 'arduino:avr',
});
const builtInLibrary = (
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
)[0];
await this.libraryService.search({ query: Arduino_BuiltIn })
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
let avrPackageError: Error | undefined;
let builtInLibraryError: Error | undefined;
@@ -84,7 +86,7 @@ export class FirstStartupInstaller extends Contribution {
}
if (builtInLibraryError) {
this.messageService.error(
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}`
);
}

View File

@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
| undefined;
override onStart(): void {
this.notificationCenter.onIndexWillUpdate((progressId) =>
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) =>
this.getOrCreateProgress(progressId)
);
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
delegate.report(progress)
);
});
this.notificationCenter.onIndexDidUpdate((progressId) => {
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => {
this.cancelProgress(progressId);
});
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {

View File

@@ -0,0 +1,228 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Contribution,
Command,
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import {
CommandRegistry,
DisposableCollection,
MaybePromise,
nls,
} from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings';
import { MainMenuManager } from '../../common/main-menu-manager';
import debounce = require('lodash.debounce');
@injectable()
export class InterfaceScale extends Contribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly menuActionsDisposables = new DisposableCollection();
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
increase: true,
decrease: true,
};
private currentSettings: Settings;
private updateSettingsDebounced = debounce(
async () => {
await this.settingsService.update(this.currentSettings);
await this.settingsService.save();
},
100,
{ maxWait: 200 }
);
override onStart(): MaybePromise<void> {
const updateCurrent = (settings: Settings) => {
this.currentSettings = settings;
this.updateFontScalingEnabled();
};
this.settingsService.onDidChange((settings) => updateCurrent(settings));
this.settingsService.settings().then((settings) => updateCurrent(settings));
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
execute: () => this.updateFontSize('increase'),
isEnabled: () => this.fontScalingEnabled.increase,
});
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
execute: () => this.updateFontSize('decrease'),
isEnabled: () => this.fontScalingEnabled.decrease,
});
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
const increaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
};
const decreaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
};
if (this.fontScalingEnabled.increase) {
this.menuActionsDisposables.push(
registry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction.label,
{ order: increaseFontSizeMenuAction.order }
)
)
);
}
if (this.fontScalingEnabled.decrease) {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction.label,
{ order: decreaseFontSizeMenuAction.order }
)
)
);
}
this.mainMenuManager.update();
}
private updateFontScalingEnabled(): void {
let fontScalingEnabled = {
increase: true,
decrease: true,
};
if (this.currentSettings.autoScaleInterface) {
fontScalingEnabled = {
increase:
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
InterfaceScale.ZoomLevel.MAX,
decrease:
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
InterfaceScale.ZoomLevel.MIN,
};
} else {
fontScalingEnabled = {
increase:
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
InterfaceScale.FontSize.MAX,
decrease:
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
InterfaceScale.FontSize.MIN,
};
}
const isChanged = Object.keys(fontScalingEnabled).some(
(key: keyof InterfaceScale.FontScalingEnabled) =>
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
);
if (isChanged) {
this.fontScalingEnabled = fontScalingEnabled;
this.registerMenus(this.menuRegistry);
}
}
private updateFontSize(mode: 'increase' | 'decrease'): void {
if (this.currentSettings.autoScaleInterface) {
mode === 'increase'
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
: (this.currentSettings.interfaceScale -=
InterfaceScale.ZoomLevel.STEP);
} else {
mode === 'increase'
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
}
this.updateFontScalingEnabled();
this.updateSettingsDebounced();
}
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
}
}
export namespace InterfaceScale {
export namespace Commands {
export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size',
};
export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size',
};
}
export namespace ZoomLevel {
export const MIN = -8;
export const MAX = 9;
export const STEP = 1;
export function toPercentage(scale: number): number {
return scale * 20 + 100;
}
export function fromPercentage(percentage: number): number {
return (percentage - 100) / 20;
}
export namespace Step {
export function toPercentage(step: number): number {
return step * 20;
}
export function fromPercentage(percentage: number): number {
return percentage / 20;
}
}
}
export namespace FontSize {
export const MIN = 8;
export const MAX = 72;
export const STEP = 2;
}
export interface FontScalingEnabled {
increase: boolean;
decrease: boolean;
}
}

View File

@@ -1,7 +1,6 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
SketchContribution,
URI,
@@ -17,11 +16,6 @@ export class NewSketch extends SketchContribution {
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),
});
}
override registerMenus(registry: MenuModelRegistry): void {
@@ -54,8 +48,5 @@ export namespace NewSketch {
export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch',
};
export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar',
};
}
}

View File

@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import { SketchesError } from '../../common/protocol';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
protected toDispose = new DisposableCollection();
override onStart(): void {
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
}
override async onReady(): Promise<void> {
this.update();
}
private update(forceUpdate?: boolean): void {
this.sketchService
.recentlyOpenedSketches()
.recentlyOpenedSketches(forceUpdate)
.then((sketches) => this.refreshMenu(sketches));
}
@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
protected register(sketches: Sketch[]): void {
const order = 0;
this.toDispose.dispose();
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
),
execute: async () => {
try {
await this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.update(true);
} else {
throw err;
}
}
},
};
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(
@@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
order: String(order),
}
);
this.toDisposeBeforeRegister.set(
sketch.uri,
this.toDispose.pushAll([
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
@@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
),
]);
}
}
}

View File

@@ -1,8 +1,8 @@
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { Later } from '../../common/nls';
import { SketchesError } from '../../common/protocol';
import { Sketch, SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
@@ -10,9 +10,19 @@ import {
URI,
} from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { promptMoveSketch } from './open-sketch';
import { ApplicationError } from '@theia/core/lib/common/application-error';
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
@injectable()
export class OpenSketchFiles extends SketchContribution {
@inject(VSCodeContextKeyService)
private readonly contextKeyService: VSCodeContextKeyService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI) => this.openSketchFiles(uri),
@@ -55,9 +65,25 @@ export class OpenSketchFiles extends SketchContribution {
}
});
}
const { workspaceError } = this.workspaceService;
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
if (SketchesError.InvalidName.is(workspaceError)) {
await this.promptMove(workspaceError);
}
} catch (err) {
// This happens when the user gracefully closed IDE2, all went well
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
if (SketchesError.InvalidName.is(err)) {
const movedSketch = await this.promptMove(err);
if (!movedSketch) {
// If user did not accept the move, or move was not possible, force reload with a fallback.
return this.openFallbackSketch();
}
}
if (SketchesError.NotFound.is(err)) {
this.openFallbackSketch();
return this.openFallbackSketch();
} else {
console.error(err);
const message =
@@ -71,6 +97,31 @@ export class OpenSketchFiles extends SketchContribution {
}
}
private async promptMove(
err: ApplicationError<
number,
{
invalidMainSketchUri: string;
}
>
): Promise<Sketch | undefined> {
const { invalidMainSketchUri } = err.data;
requestAnimationFrame(() => this.messageService.error(err.message));
await wait(10); // let IDE2 toast the error message.
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
});
if (movedSketch) {
this.workspaceService.open(new URI(movedSketch.uri), {
preserveWindow: true,
});
return movedSketch;
}
return undefined;
}
private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
@@ -84,8 +135,48 @@ export class OpenSketchFiles extends SketchContribution {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
const disposables = new DisposableCollection();
if (!widget || forceOpen) {
return this.editorManager.open(
const deferred = new Deferred<EditorWidget>();
disposables.push(
this.editorManager.onCreated((editor) => {
if (editor.editor.uri.toString() === uri) {
if (editor.isVisible) {
disposables.dispose();
deferred.resolve(editor);
} else {
// In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
// This is a hack to first get an event from monaco after the widget update request, then IDE2 waits for the next monaco context key event.
// Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
disposables.push(
(editor.editor as MonacoEditor).onDidResize((dimension) => {
if (dimension) {
const isKeyOwner = (
arg: unknown
): arg is { key: string } => {
if (typeof arg === 'object') {
const object = arg as Record<string, unknown>;
return typeof object['key'] === 'string';
}
return false;
};
disposables.push(
this.contextKeyService.onDidChangeContext((e) => {
// `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
if (isKeyOwner(e) && e.key === 'commentIsEmpty') {
deferred.resolve(editor);
disposables.dispose();
}
})
);
}
})
);
}
}
})
);
this.editorManager.open(
new URI(uri),
options ?? {
mode: 'reveal',
@@ -93,6 +184,20 @@ export class OpenSketchFiles extends SketchContribution {
counter: 0,
}
);
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
const result = await Promise.race([
deferred.promise,
wait(timeout).then(() => {
disposables.dispose();
return 'timeout';
}),
]);
if (result === 'timeout') {
console.warn(
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
);
}
return result;
}
}
}

View File

@@ -1,115 +1,50 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
SketchesError,
SketchesService,
SketchRef,
} from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
SketchContribution,
Sketch,
URI,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
SketchContribution,
URI,
} from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples';
import { Sketchbook } from './sketchbook';
import { SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
export type SketchLocation = string | URI | SketchRef;
export namespace SketchLocation {
export function toUri(location: SketchLocation): URI {
if (typeof location === 'string') {
return new URI(location);
} else if (SketchRef.is(location)) {
return toUri(location.uri);
} else {
return location;
}
}
export function is(arg: unknown): arg is SketchLocation {
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
}
}
@injectable()
export class OpenSketch extends SketchContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(BuiltInExamples)
protected readonly builtInExamples: BuiltInExamples;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(Sketchbook)
protected readonly sketchbook: Sketchbook;
protected readonly toDispose = new DisposableCollection();
override 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 container = await this.sketchService.getSketches({
exclude: ['**/hardware/**'],
});
if (SketchContainer.isEmpty(container)) {
this.openSketch();
} else {
this.toDispose.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: nls.localize(
'vscode/workspaceActions/openFileFolder',
'Open...'
),
}
);
this.toDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
OpenSketch.Commands.OPEN_SKETCH
)
)
);
this.sketchbook.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
this.toDispose
);
try {
const containers = await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(
container,
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
this.toDispose
);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
execute: async (arg) => {
const toOpen = !SketchLocation.is(arg)
? await this.selectSketch()
: arg;
if (toOpen) {
return this.openSketch(toOpen);
}
},
});
@@ -130,30 +65,40 @@ export class OpenSketch extends SketchContribution {
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
if (!toOpen) {
return;
}
const uri = SketchLocation.toUri(toOpen);
try {
await this.sketchService.loadSketch(uri.toString());
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
}
throw err;
}
this.workspaceService.open(uri);
}
protected async selectSketch(): Promise<Sketch | undefined> {
private async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
});
const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
}
);
if (!filePaths.length) {
return undefined;
}
@@ -169,45 +114,11 @@ export class OpenSketch extends SketchContribution {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/issueMainService/ok', 'OK'),
],
message: nls.localize(
'arduino/sketch/movingMsg',
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
nameWithExt,
name
),
return promptMoveSketch(sketchFileUri, {
fileService: this.fileService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
});
if (response === 1) {
// OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize(
'arduino/sketch/cantOpen',
'A folder named "{0}" already exists. Can\'t open sketch.',
name
),
});
return undefined;
}
await this.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
}
@@ -217,8 +128,57 @@ export namespace OpenSketch {
export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch',
};
export const OPEN_SKETCH__TOOLBAR: Command = {
id: 'arduino-open-sketch--toolbar',
};
}
}
export async function promptMoveSketch(
sketchFileUri: string | URI,
options: {
fileService: FileService;
sketchService: SketchesService;
labelProvider: LabelProvider;
}
): Promise<Sketch | undefined> {
const { fileService, sketchService, labelProvider } = options;
const uri =
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
const name = uri.path.name;
const nameWithExt = labelProvider.getName(uri);
const { response } = await remote.dialog.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/issueMainService/ok', 'OK'),
],
message: nls.localize(
'arduino/sketch/movingMsg',
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
nameWithExt,
name
),
});
if (response === 1) {
// OK
const newSketchUri = uri.parent.resolve(name);
const exists = await fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize(
'arduino/sketch/cantOpen',
'A folder named "{0}" already exists. Can\'t open sketch.',
name
),
});
return undefined;
}
await fileService.createFolder(newSketchUri);
await fileService.move(
uri,
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return sketchService.getSketchFolder(newSketchUri.toString());
}
}

View File

@@ -12,21 +12,19 @@ import {
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import { StartupTask } from '../../electron-common/startup-task';
import { DeleteSketch } from './delete-sketch';
@injectable()
export class SaveAsSketch extends SketchContribution {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
protected readonly windowService: WindowService;
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
@@ -52,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
private async saveAs(
{
execOnlyIfTemp,
openAfterMove,
@@ -60,7 +58,10 @@ export class SaveAsSketch extends SketchContribution {
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@@ -70,27 +71,38 @@ export class SaveAsSketch extends SketchContribution {
return false;
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = new URI(configuration.sketchDirUri);
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch.
const containerDirUri = isTemp
? sketchbookDirUri
: !sketchbookDirUri.isEqualOrParent(sketchUri)
? sketchbookDirUri
: sketchUri.parent;
const exists = await this.fileService.exists(
containerDirUri.resolve(sketch.name)
);
// 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.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = sketchDirUri.resolve(
const defaultUri = containerDirUri.resolve(
exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name
);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath || canceled) {
return false;
}
@@ -107,21 +119,19 @@ export class SaveAsSketch extends SketchContribution {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
// This window will navigate away.
// Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch.
// Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification.
// https://github.com/arduino/arduino-ide/issues/39.
this.sketchServiceClient.onStop();
// TODO: consider implementing the temp sketch deletion the following way:
// Open the other sketch with a `delete the temp sketch` startup-task.
this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend.
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
});
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
this.workspaceService.open(new URI(workspaceUri), options);
}
return !!workspaceUri;
}

View File

@@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
@@ -19,12 +18,6 @@ export class SaveSketch extends SketchContribution {
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),
});
}
override registerMenus(registry: MenuModelRegistry): void {
@@ -68,8 +61,5 @@ export namespace SaveSketch {
export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch',
};
export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar',
};
}
}

View File

@@ -176,7 +176,7 @@ export class SketchControl extends SketchContribution {
{
commandId: command.id,
label: this.labelProvider.getName(uri),
order: `${i}`,
order: String(i).padStart(4),
}
);
this.toDisposeBeforeCreateNewContextMenu.push(

View File

@@ -4,7 +4,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution, URI } from './contribution';
import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@injectable()
@@ -31,7 +31,6 @@ export class SketchFilesTracker extends SketchContribution {
override onReady(): void {
this.sketchServiceClient.currentSketch().then(async (sketch) => {
if (CurrentSketch.isValid(sketch)) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {

View File

@@ -1,32 +1,14 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandRegistry, MenuModelRegistry } from './contribution';
import { MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { Examples } from './examples';
import {
SketchContainer,
SketchesError,
SketchRef,
} from '../../common/protocol';
import { SketchContainer, SketchesError } from '../../common/protocol';
import { OpenSketch } from './open-sketch';
import { nls } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class Sketchbook extends Examples {
@inject(CommandRegistry)
protected override readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected override readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
}
@@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
this.update();
}
private update() {
protected override update(): void {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
this.menuManager.update();
});
}
@@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
);
}
protected register(container: SketchContainer): void {
private register(container: SketchContainer): void {
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
@@ -62,23 +44,18 @@ export class Sketchbook extends Examples {
protected override createHandler(uri: string): CommandHandler {
return {
execute: async () => {
let sketch: SketchRef | undefined = undefined;
try {
sketch = await this.sketchService.loadSketch(uri);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// To handle the following:
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
this.messageService.error(err.message);
this.update();
}
}
if (sketch) {
await this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
uri
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// Force update the menu items to remove the absent sketch.
this.update();
} else {
throw err;
}
}
},
};

View File

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

View File

@@ -0,0 +1,193 @@
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CoreService, IndexType } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import { Command, CommandRegistry, Contribution } from './contribution';
@injectable()
export class UpdateIndexes extends Contribution {
@inject(WindowServiceExt)
private readonly windowService: WindowServiceExt;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
@inject(CoreService)
private readonly coreService: CoreService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
protected override init(): void {
super.init();
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
Promise.all(
Object.entries(summary).map(([type, updatedAt]) =>
this.setLastUpdateDateTime(type as IndexType, updatedAt)
)
)
);
}
override onReady(): void {
this.checkForUpdates();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
execute: () => this.updateIndexes(IndexType.All, true),
});
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
execute: () => this.updateIndexes(['platform'], true),
});
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
execute: () => this.updateIndexes(['library'], true),
});
}
private async checkForUpdates(): Promise<void> {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
console.debug(
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
);
return;
}
if (await this.windowService.isFirstWindow()) {
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
if (summary.message) {
this.messageService.error(summary.message);
}
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
if (Object.keys(summary).length) {
console.debug(
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
summary
)}`
);
} else {
console.debug(
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
);
}
await Promise.allSettled([
...Object.entries(summary).map(([type, updatedAt]) =>
this.setLastUpdateDateTime(type as IndexType, updatedAt)
),
this.updateIndexes(typesToCheck),
]);
}
}
private async updateIndexes(
types: IndexType[],
force = false
): Promise<void> {
const updatedAt = new Date().toISOString();
return Promise.all(
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
).then((needsIndexUpdateResults) => {
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
if (typesToUpdate.length) {
console.debug(
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
typesToUpdate
)} with date time: ${updatedAt}.`
);
return this.coreService.updateIndex({ types: typesToUpdate });
}
});
}
private async needsIndexUpdate(
type: IndexType,
now: string,
force = false
): Promise<IndexType | false> {
if (force) {
console.debug(
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
);
return type;
}
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
if (!lastUpdateIsoDateTime) {
console.debug(
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
);
return type;
}
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
if (Number.isNaN(lastUpdateDateTime)) {
console.debug(
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
);
return type;
}
const diff = new Date(now).getTime() - lastUpdateDateTime;
const needsIndexUpdate = diff >= this.threshold;
console.debug(
`[update-indexes]: Update for index type '${type}' is ${
needsIndexUpdate ? '' : 'not '
}required. Now: ${now}, Last index update date time: ${new Date(
lastUpdateDateTime
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
);
return needsIndexUpdate ? type : false;
}
private async getLastUpdateDateTime(
type: IndexType
): Promise<string | undefined> {
const key = this.storageKeyOf(type);
return this.localStorage.getData<string>(key);
}
private async setLastUpdateDateTime(
type: IndexType,
updatedAt: string
): Promise<void> {
const key = this.storageKeyOf(type);
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
console.debug(
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
);
});
}
private storageKeyOf(type: IndexType): string {
return `index-last-update-time--${type}`;
}
private get threshold(): number {
return 4 * 60 * 60 * 1_000; // four hours in millis
}
}
export namespace UpdateIndexes {
export namespace Commands {
export const UPDATE_INDEXES: Command & { label: string } = {
id: 'arduino-update-indexes',
label: nls.localize(
'arduino/updateIndexes/updateIndexes',
'Update Indexes'
),
category: 'Arduino',
};
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
id: 'arduino-update-package-index',
label: nls.localize(
'arduino/updateIndexes/updatePackageIndex',
'Update Package Index'
),
category: 'Arduino',
};
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
id: 'arduino-update-library-index',
label: nls.localize(
'arduino/updateIndexes/updateLibraryIndex',
'Update Library Index'
),
category: 'Arduino',
};
}
}

View File

@@ -1,7 +1,7 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
@@ -11,96 +11,36 @@ import {
TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';
@injectable()
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
private boardRequiresUserFields = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) {
return '';
}
return fqbn + '|' + address;
}
@inject(UserFields)
private readonly userFields: UserFields;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (
this.boardRequiresUserFields &&
key &&
!this.cachedUserFields.has(key)
) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceProvider.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
if (await this.userFields.checkUserFieldsDialog()) {
this.uploadSketch();
}
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress,
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
if (await this.userFields.checkUserFieldsDialog(true)) {
this.uploadSketch();
}
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
});
registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
@@ -120,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
})
);
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
})
);
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
});
}
override registerKeybindings(registry: KeybindingRegistry): void {
@@ -215,18 +130,7 @@ export class UploadSketch extends CoreServiceContribution {
return;
}
// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (
uploadOptions.userFields.length === 0 &&
this.boardRequiresUserFields
) {
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
if (!this.userFields.checkUserFieldsForUpload()) {
return;
}
@@ -242,6 +146,7 @@ export class UploadSketch extends CoreServiceContribution {
{ timeout: 3000 }
);
} catch (e) {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
this.uploadInProgress = false;
@@ -258,7 +163,7 @@ export class UploadSketch extends CoreServiceContribution {
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const userFields = this.userFields();
const userFields = this.userFields.getUserFields();
const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
@@ -301,10 +206,6 @@ export class UploadSketch extends CoreServiceContribution {
return port;
}
private userFields(): BoardUserField[] {
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
}
/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
@@ -328,7 +229,7 @@ export namespace UploadSketch {
id: 'arduino-upload-with-configuration-sketch',
label: nls.localize(
'arduino/sketch/configureAndUpload',
'Configure And Upload'
'Configure and Upload'
),
category: 'Arduino',
};

View File

@@ -0,0 +1,147 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution';
import { UploadSketch } from './upload-sketch';
@injectable()
export class UserFields extends Contribution {
private boardRequiresUserFields = false;
private userFieldsSet = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
}
private selectedFqbnAddress(): string | undefined {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return undefined;
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address ||
'';
return fqbn + '|' + address;
}
private async showUserFieldsDialog(
key: string
): Promise<BoardUserField[] | undefined> {
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = cached
? cached.slice()
: await this.boardsServiceProvider.selectedBoardUserFields();
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.userFieldsSet = true;
this.cachedUserFields.set(key, result);
return result;
}
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
const key = this.selectedFqbnAddress();
if (!key) {
return false;
}
/*
If the board requires to be configured with user fields, we want
to show the user fields dialog, but only if they weren't already
filled in or if they were filled in, but the previous upload failed.
*/
if (
!forceOpen &&
(!this.boardRequiresUserFields ||
(this.cachedUserFields.has(key) && this.userFieldsSet))
) {
return true;
}
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
return userFieldsFilledIn;
}
checkUserFieldsForUpload(): boolean {
// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
this.userFieldsSet = true;
return true;
}
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
this.userFieldsSet = false;
return false;
}
getUserFields(): BoardUserField[] {
const fqbnAddress = this.selectedFqbnAddress();
if (!fqbnAddress) {
return [];
}
return this.cachedUserFields.get(fqbnAddress) ?? [];
}
isRequired(): boolean {
return this.boardRequiresUserFields;
}
notifyFailedWithError(e: Error): void {
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
this.userFieldsSet = false;
}
}
}

View File

@@ -171,6 +171,9 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();

View File

@@ -115,6 +115,8 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();

View File

@@ -20,8 +20,7 @@ import {
import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL =
'https://www.arduino.cc/en/software#experimental-software';
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {

View File

@@ -10,6 +10,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
AdditionalUrls,
CompilerWarnings,
CompilerWarningLiterals,
Network,
ProxySettings,
@@ -22,14 +23,22 @@ import {
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input';
import { InterfaceScale } from '../../contributions/interface-scale';
const maxScale = 280;
const minScale = -60;
const scaleStep = 20;
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MAX
);
const minScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MIN
);
const scaleStep = InterfaceScale.ZoomLevel.Step.toPercentage(
InterfaceScale.ZoomLevel.STEP
);
const maxFontSize = InterfaceScale.FontSize.MAX;
const minFontSize = InterfaceScale.FontSize.MIN;
const fontSizeStep = InterfaceScale.FontSize.STEP;
const maxFontSize = 72;
const minFontSize = 0;
const fontSizeStep = 2;
export class SettingsComponent extends React.Component<
SettingsComponent.Props,
SettingsComponent.State
@@ -171,7 +180,8 @@ export class SettingsComponent extends React.Component<
<div className="column">
<div className="flex-line">
<SettingsStepInput
value={this.state.editorFontSize}
key={`font-size-stepper-${String(this.state.editorFontSize)}`}
initialValue={this.state.editorFontSize}
setSettingsStateValue={this.setFontSize}
step={fontSizeStep}
maxValue={maxFontSize}
@@ -190,13 +200,18 @@ export class SettingsComponent extends React.Component<
</label>
<div>
<SettingsStepInput
value={scalePercentage}
key={`scale-stepper-${String(scalePercentage)}`}
initialValue={scalePercentage}
setSettingsStateValue={this.setInterfaceScale}
step={scaleStep}
maxValue={maxScale}
minValue={minScale}
unitOfMeasure="%"
classNames={{ input: 'theia-input small with-margin' }}
classNames={{
input: 'theia-input small with-margin',
buttonsContainer:
'settings-step-input-buttons-container-perc',
}}
/>
</div>
</div>
@@ -260,7 +275,7 @@ export class SettingsComponent extends React.Component<
>
{CompilerWarningLiterals.map((value) => (
<option key={value} value={value}>
{value}
{CompilerWarnings.labelOf(value)}
</option>
))}
</select>
@@ -398,10 +413,22 @@ export class SettingsComponent extends React.Component<
</form>
<div className="flex-line proxy-settings">
<div className="column">
<div className="flex-line">Host name:</div>
<div className="flex-line">Port number:</div>
<div className="flex-line">Username:</div>
<div className="flex-line">Password:</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/hostname',
'Host name'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/port',
'Port number'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/username',
'Username'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/password',
'Password'
)}:`}</div>
</div>
<div className="column stretch">
<div className="flex-line">
@@ -502,6 +529,7 @@ export class SettingsComponent extends React.Component<
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true,
modal: true,
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
@@ -540,8 +568,7 @@ export class SettingsComponent extends React.Component<
};
private setInterfaceScale = (percentage: number) => {
const interfaceScale = (percentage - 100) / 20;
const interfaceScale = InterfaceScale.ZoomLevel.fromPercentage(percentage);
this.setState({ interfaceScale });
};

View File

@@ -155,7 +155,6 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls
.filter((url) => url.trim())
.filter((url) => !!url)
@@ -181,10 +180,10 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
get value(): string[] {

View File

@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import classnames from 'classnames';
interface SettingsStepInputProps {
value: number;
initialValue: number;
setSettingsStateValue: (value: number) => void;
step: number;
maxValue: number;
@@ -15,7 +15,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
props: SettingsStepInputProps
) => {
const {
value,
initialValue,
setSettingsStateValue,
step,
maxValue,
@@ -24,18 +24,35 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
classNames,
} = props;
const [valueState, setValueState] = React.useState<{
currentValue: number;
isEmptyString: boolean;
}>({
currentValue: initialValue,
isEmptyString: false,
});
const { currentValue, isEmptyString } = valueState;
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max);
};
const resetToInitialState = (): void => {
setValueState({
currentValue: initialValue,
isEmptyString: false,
});
};
const onStep = (
roundingOperation: 'ceil' | 'floor',
stepOperation: (a: number, b: number) => number
): void => {
const valueRoundedToScale = Math[roundingOperation](value / step) * step;
const valueRoundedToScale =
Math[roundingOperation](currentValue / step) * step;
const calculatedValue =
valueRoundedToScale === value
? stepOperation(value, step)
valueRoundedToScale === currentValue
? stepOperation(currentValue, step)
: valueRoundedToScale;
const newValue = clamp(calculatedValue, minValue, maxValue);
@@ -52,33 +69,53 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { value: eventValue } = event.target;
if (eventValue === '') {
setSettingsStateValue(0);
}
const number = Number(eventValue);
if (!isNaN(number) && number !== value) {
const newValue = clamp(number, minValue, maxValue);
setSettingsStateValue(newValue);
}
setValueState({
currentValue: Number(eventValue),
isEmptyString: eventValue === '',
});
};
const upDisabled = value >= maxValue;
const downDisabled = value <= minValue;
/* Prevent the user from entering invalid values */
const onBlur = (event: React.FocusEvent): void => {
if (
(currentValue === initialValue && !isEmptyString) ||
event.currentTarget.contains(event.relatedTarget as Node)
) {
return;
}
const clampedValue = clamp(currentValue, minValue, maxValue);
if (clampedValue === initialValue || isNaN(currentValue) || isEmptyString) {
resetToInitialState();
return;
}
setSettingsStateValue(clampedValue);
};
const valueIsNotWithinRange =
currentValue < minValue || currentValue > maxValue;
const isDisabledException =
valueIsNotWithinRange || isEmptyString || isNaN(currentValue);
const upDisabled = isDisabledException || currentValue >= maxValue;
const downDisabled = isDisabledException || currentValue <= minValue;
return (
<div className="settings-step-input-container">
<div className="settings-step-input-container" onBlur={onBlur}>
<input
className={classnames('settings-step-input-element', classNames?.input)}
value={value.toString()}
value={isEmptyString ? '' : String(currentValue)}
onChange={onUserInput}
type="number"
pattern="[0-9]+"
/>
<div className="settings-step-input-buttons-container">
<div
className={classnames(
'settings-step-input-buttons-container',
classNames?.buttonsContainer
)}
>
<button
className="settings-step-input-button settings-step-input-up-button"
disabled={upDisabled}

View File

@@ -16,9 +16,9 @@ export const UserFieldsComponent = ({
const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[]
>(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true);
const firstInputElement = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
setBoardUserFields(initialBoardUserFields);
@@ -48,7 +48,10 @@ export const UserFieldsComponent = ({
React.useEffect(() => {
updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
}, [boardUserFields]);
if (firstInputElement.current) {
firstInputElement.current.focus();
}
}, [boardUserFields, updateUserFields]);
return (
<div>
@@ -71,6 +74,7 @@ export const UserFieldsComponent = ({
field.label
)}
onChange={updateUserField(index)}
ref={index === 0 ? firstInputElement : undefined}
/>
</div>
</div>

View File

@@ -13,7 +13,7 @@ import { BoardUserField } from '../../../common/protocol';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
protected _currentUserFields: BoardUserField[] = [];
private _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
@@ -34,7 +34,7 @@ export class UserFieldsDialogWidget extends ReactWidget {
});
}
protected setUserFields(userFields: BoardUserField[]): void {
private setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}

View File

@@ -119,20 +119,16 @@ export class LibraryListWidget extends ListWidget<
message.appendChild(question);
const result = await new MessageBoxDialog({
title: nls.localize(
'arduino/library/dependenciesForLibrary',
'Dependencies for library {0}:{1}',
item.name,
version
'arduino/library/installLibraryDependencies',
'Install library dependencies'
),
message,
buttons: [
nls.localize('arduino/library/installAll', 'Install all'),
nls.localize(
'arduino/library/installOnly',
'Install {0} only',
item.name
'arduino/library/installWithoutDependencies',
'Install without dependencies'
),
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('arduino/library/installAll', 'Install All'),
],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
@@ -140,11 +136,11 @@ export class LibraryListWidget extends ListWidget<
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
} else if (response === 1) {
// All
installDependencies = true;
}
}
} else {
@@ -201,7 +197,13 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')]
).forEach((text, index) => {
const button = this.createButton(text);
button.classList.add(index === 0 ? 'main' : 'secondary');
const isPrimaryButton =
index === (options.buttons ? options.buttons.length - 1 : 0);
button.title = text;
button.classList.add(
isPrimaryButton ? 'main' : 'secondary',
'message-box-dialog-button'
);
this.controlPanel.appendChild(button);
this.toDisposeOnDetach.push(
addEventListener(button, 'click', () => {

View File

@@ -1,16 +1,17 @@
import { injectable } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { injectable } from '@theia/core/shared/inversify';
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
import { URI } from '../contributions/contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { LibraryListWidget } from './library-list-widget';
@injectable()
export class LibraryListWidgetFrontendContribution
extends AbstractViewContribution<LibraryListWidget>
implements FrontendApplicationContribution
{
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
LibraryPackage,
LibrarySearch
> {
constructor() {
super({
widgetId: LibraryListWidget.WIDGET_ID,
@@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
});
}
async initializeLayout(): Promise<void> {
this.openView();
}
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
@@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
});
}
}
protected canParse(uri: URI): boolean {
try {
LibrarySearch.UriParser.parse(uri);
return true;
} catch {
return false;
}
}
protected parse(uri: URI): LibrarySearch | undefined {
return LibrarySearch.UriParser.parse(uri);
}
}

View File

@@ -8,6 +8,9 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
IndexUpdateWillStartParams,
NotificationServiceClient,
NotificationServiceServer,
} from '../common/protocol/notification-service';
@@ -29,48 +32,48 @@ export class NotificationCenter
implements NotificationServiceClient, FrontendApplicationContribution
{
@inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
private readonly server: JsonRpcProxy<NotificationServiceServer>;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly indexDidUpdateEmitter = new Emitter<string>();
protected readonly indexWillUpdateEmitter = new Emitter<string>();
protected readonly indexUpdateDidProgressEmitter =
private readonly indexUpdateDidCompleteEmitter =
new Emitter<IndexUpdateDidCompleteParams>();
private readonly indexUpdateWillStartEmitter =
new Emitter<IndexUpdateWillStartParams>();
private readonly indexUpdateDidProgressEmitter =
new Emitter<ProgressMessage>();
protected readonly indexUpdateDidFailEmitter = new Emitter<{
progressId: string;
message: string;
}>();
protected readonly daemonDidStartEmitter = new Emitter<string>();
protected readonly daemonDidStopEmitter = new Emitter<void>();
protected readonly configDidChangeEmitter = new Emitter<{
private readonly indexUpdateDidFailEmitter =
new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<{
config: Config | undefined;
}>();
protected readonly platformDidInstallEmitter = new Emitter<{
private readonly platformDidInstallEmitter = new Emitter<{
item: BoardsPackage;
}>();
protected readonly platformDidUninstallEmitter = new Emitter<{
private readonly platformDidUninstallEmitter = new Emitter<{
item: BoardsPackage;
}>();
protected readonly libraryDidInstallEmitter = new Emitter<{
private readonly libraryDidInstallEmitter = new Emitter<{
item: LibraryPackage;
}>();
protected readonly libraryDidUninstallEmitter = new Emitter<{
private readonly libraryDidUninstallEmitter = new Emitter<{
item: LibraryPackage;
}>();
protected readonly attachedBoardsDidChangeEmitter =
private readonly attachedBoardsDidChangeEmitter =
new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{
private readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[];
}>();
private readonly onAppStateDidChangeEmitter =
new Emitter<FrontendApplicationState>();
protected readonly toDispose = new DisposableCollection(
this.indexWillUpdateEmitter,
private readonly toDispose = new DisposableCollection(
this.indexUpdateWillStartEmitter,
this.indexUpdateDidProgressEmitter,
this.indexDidUpdateEmitter,
this.indexUpdateDidCompleteEmitter,
this.indexUpdateDidFailEmitter,
this.daemonDidStartEmitter,
this.daemonDidStopEmitter,
@@ -82,8 +85,8 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter
);
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
@@ -112,26 +115,20 @@ export class NotificationCenter
this.toDispose.dispose();
}
notifyIndexWillUpdate(progressId: string): void {
this.indexWillUpdateEmitter.fire(progressId);
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.indexUpdateWillStartEmitter.fire(params);
}
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
this.indexUpdateDidProgressEmitter.fire(progressMessage);
}
notifyIndexDidUpdate(progressId: string): void {
this.indexDidUpdateEmitter.fire(progressId);
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
this.indexUpdateDidCompleteEmitter.fire(params);
}
notifyIndexUpdateDidFail({
progressId,
message,
}: {
progressId: string;
message: string;
}): void {
this.indexUpdateDidFailEmitter.fire({ progressId, message });
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
this.indexUpdateDidFailEmitter.fire(params);
}
notifyDaemonDidStart(port: string): void {

View File

@@ -1,12 +1,58 @@
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board } from '../../../common/protocol/boards-service';
import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
class HistoryList {
private readonly items: string[] = [];
private index = -1;
constructor(private readonly size = 100) {}
push(val: string): void {
if (val !== this.items[this.items.length - 1]) {
this.items.push(val);
}
while (this.items.length > this.size) {
this.items.shift();
}
this.index = -1;
}
previous(): string {
if (this.index === -1) {
this.index = this.items.length - 1;
return this.items[this.index];
}
if (this.hasPrevious) {
return this.items[--this.index];
}
return this.items[this.index];
}
private get hasPrevious(): boolean {
return this.index >= 1;
}
next(): string {
if (this.index === this.items.length - 1) {
this.index = -1;
return '';
}
if (this.hasNext) {
return this.items[++this.index];
}
return '';
}
private get hasNext(): boolean {
return this.index >= 0 && this.index !== this.items.length - 1;
}
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
@@ -17,6 +63,7 @@ export namespace SerialMonitorSendInput {
export interface State {
text: string;
connected: boolean;
history: HistoryList;
}
}
@@ -28,7 +75,7 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: true };
this.state = { text: '', connected: true, history: new HistoryList() };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
@@ -81,8 +128,7 @@ export class SerialMonitorSendInput extends React.Component<
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return nls.localize(
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}')",
isOSX ? '⌘' : nls.localize('vscode/keybindingLabels/ctrlKey', 'Ctrl'),
"Message (Enter to send message to '{0}' on '{1}')",
board
? Board.toString(board, {
useFqbn: false,
@@ -92,7 +138,7 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected setRef = (element: HTMLElement | null) => {
protected setRef = (element: HTMLElement | null): void => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
@@ -110,9 +156,19 @@ export class SerialMonitorSendInput extends React.Component<
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
const { key } = keyCode;
if (key === Key.ENTER) {
const { text } = this.state;
this.onSend();
if (text) {
this.state.history.push(text);
}
} else if (key === Key.ARROW_UP) {
this.setState({ text: this.state.history.previous() });
} else if (key === Key.ARROW_DOWN) {
this.setState({ text: this.state.history.next() });
} else if (key === Key.ESCAPE) {
this.setState({ text: '' });
}
}
}

View File

@@ -109,7 +109,7 @@ const _Row = ({
}) => {
const timestamp =
(data.timestamp &&
`${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) ||
`${dateFormat(data.lines[index].timestamp, 'HH:MM:ss.l')} -> `) ||
'';
return (
(data.lines[index].lineLen && (

View File

@@ -14,6 +14,11 @@ import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import {
CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW,
} from '../../../common/ipc-communication';
import { nls } from '@theia/core/lib/common/nls';
const queryString = require('query-string');
@@ -58,7 +63,7 @@ export class PlotterFrontendContribution extends Contribution {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
if (!!this.window) {
this.window = null;
}
@@ -96,14 +101,19 @@ export class PlotterFrontendContribution extends Contribution {
async startPlotter(): Promise<void> {
await this.monitorManagerProxy.startMonitor();
if (!!this.window) {
this.window.focus();
ipcRenderer.send(SHOW_PLOTTER_WINDOW);
return;
}
const wsPort = this.monitorManagerProxy.getWebSocketPort();
if (wsPort) {
this.open(wsPort);
} else {
this.messageService.error(`Couldn't open serial plotter`);
this.messageService.error(
nls.localize(
'arduino/contributions/plotter/couldNotOpen',
"Couldn't open serial plotter"
)
);
}
}

View File

@@ -1,5 +1,11 @@
#select-board-dialog-container > .dialogBlock {
width: 640px;
height: 500px;
}
div#select-board-dialog {
margin: 5px;
height: 100%;
}
div#select-board-dialog .selectBoardContainer {
@@ -7,12 +13,17 @@ div#select-board-dialog .selectBoardContainer {
gap: 10px;
overflow: hidden;
max-height: 100%;
height: 100%;
}
.select-board-dialog .head {
margin: 5px;
}
.dialogContent.select-board-dialog {
height: 100%;
}
div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400;
letter-spacing: 0.02em;
@@ -63,6 +74,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
}
#select-board-dialog .selectBoardContainer .left.container .content {
@@ -131,6 +143,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
#select-board-dialog .selectBoardContainer .list {
max-height: 200px;
overflow-y: auto;
flex: 1;
}
#select-board-dialog .selectBoardContainer .ports.list {
@@ -282,3 +295,11 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: none;
}
}
#select-board-dialog .no-result {
text-transform: uppercase;
height: 100%;
user-select: none;
padding: 10px 5px;
overflow-wrap: break-word;
}

View File

@@ -9,7 +9,8 @@
total = padding + margin = 96px
*/
max-width: calc(100% - 96px) !important;
min-width: unset;
min-width: 424px;
max-height: 560px;
padding: 0 28px;
}
@@ -85,3 +86,4 @@
max-height: 400px;
}
}

View File

@@ -7,7 +7,7 @@
}
.firmware-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
background: var(--theia-input-background) !important;
}
.firmware-uploader-dialog .dialogRow > button{
@@ -28,4 +28,4 @@
.firmware-uploader-dialog .status-icon {
margin-right: 10px;
}
}

View File

@@ -55,7 +55,8 @@
/* Makes the sidepanel a bit wider when opening the widget */
.p-DockPanel-widget {
min-width: 200px;
min-height: 200px;
min-height: 20px;
height: 200px;
}
/* Overrule the default Theia CSS button styles. */
@@ -108,6 +109,13 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
background-color: var(--theia-secondaryButton-background);
}
button.theia-button.message-box-dialog-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* To make the progress-bar slightly thicker, and use the color from the status bar */
.theia-progress-bar-container {
width: 100%;
@@ -136,6 +144,9 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
font-size: 14px;
}
.uppercase {
text-transform: uppercase;
}
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/

View File

@@ -111,13 +111,13 @@
font-weight: bold;
max-height: calc(1em + 4px);
color: var(--theia-button-foreground);
content: 'INSTALLED';
content: attr(install);
}
.component-list-item .header .installed:hover:before {
background-color: var(--theia-button-foreground);
color: var(--theia-button-background);
content: 'UNINSTALL';
content: attr(uninstall);
}
.component-list-item[min-width~="170px"] .footer {
@@ -131,7 +131,7 @@
flex-direction: column-reverse;
}
.component-list-item:hover .footer > * {
.component-list-item .footer > * {
display: inline-block;
margin: 5px 0px 0px 10px;
}
@@ -160,4 +160,4 @@
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
border: 1px solid var(--theia-button-border);
}
}

View File

@@ -2,6 +2,16 @@
background: var(--theia-editorGroupHeader-tabsBackground);
}
/* Avoid the Intellisense widget may be cover by the bottom panel partially.
TODO: This issue may be resolved after monaco-editor upgrade */
#theia-main-content-panel {
z-index: auto
}
#theia-main-content-panel div[id^="code-editor-opener"] {
z-index: auto;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 0;
}
@@ -87,8 +97,7 @@
display: flex;
justify-content: center;
align-items: center;
background-color: var(--theia-titleBar-activeBackground);
background-color: var(--theia-titleBar-activeBackground);
}
#arduino-toolbar-container {
@@ -243,3 +252,10 @@
outline: 1px solid var(--theia-contrastBorder);
outline-offset: -1px;
}
.monaco-hover .hover-row.markdown-hover:first-child p {
margin-top: 8px;
}
.monaco-hover .hover-row.markdown-hover:first-child .monaco-tokenized-source {
margin-top: 8px;
}

View File

@@ -14,6 +14,10 @@
font-family: monospace
}
.serial-monitor-messages pre {
margin: 0px;
}
.serial-monitor .head {
display: flex;
padding: 5px;

View File

@@ -88,9 +88,13 @@
}
.additional-urls-dialog textarea {
width: 100%;
resize: none;
white-space: nowrap;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog {
display: block;
display: flex;
overflow: hidden;
padding: 0 1px;
margin: 0 -1px;
}

View File

@@ -2,17 +2,17 @@
position: relative
}
.settings-step-input-element::-webkit-inner-spin-button,
.settings-step-input-element::-webkit-inner-spin-button,
.settings-step-input-element::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
margin: 0;
}
.settings-step-input-buttons-container {
display: none;
flex-direction: column;
position: absolute;
right: 14px;
right: 0px;
top: 50%;
transform: translate(0px, -50%);
height: calc(100% - 4px);
@@ -21,7 +21,11 @@
background: var(--theia-input-background);
}
.settings-step-input-container:hover > .settings-step-input-buttons-container {
.settings-step-input-buttons-container-perc {
right: 14px;
}
.settings-step-input-container:hover>.settings-step-input-buttons-container {
display: flex;
}
@@ -43,4 +47,4 @@
.settings-step-input-button:hover {
background: rgba(128, 128, 128, 0.8);
}
}

View File

@@ -1,8 +1,6 @@
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { duration } from '../../../common/decorators';
export class AboutDialog extends TheiaAboutDialog {
@duration({ name: 'theia-about#init' })
protected override async init(): Promise<void> {
// NOOP
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.

View File

@@ -1,26 +0,0 @@
import { injectable } from '@theia/core/shared/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;
override createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar();
return this.menuBar;
}
update(): void {
if (this.menuBar) {
this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar);
}
}
}

View File

@@ -1,18 +0,0 @@
import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from '@theia/core/shared/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

@@ -1,10 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { ContainerModule } from '@theia/core/shared/inversify';
import { DefaultWindowService } from './default-window-service';
import { WindowServiceExt } from './window-service-ext';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultWindowService).toSelf().inSingletonScope();
rebind(TheiaDefaultWindowService).toService(DefaultWindowService);
bind(WindowServiceExt).toService(DefaultWindowService);
});

View File

@@ -1,17 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { injectable } from '@theia/core/shared/inversify';
import { WindowServiceExt } from './window-service-ext';
@injectable()
export class DefaultWindowService
extends TheiaDefaultWindowService
implements WindowServiceExt
{
/**
* The default implementation always resolves to `true`.
* IDE2 does not use it. It's currently an electron-only app.
*/
async isFirstWindow(): Promise<boolean> {
return true;
}
}

View File

@@ -1,7 +1,10 @@
import type { StartupTask } from '../../../electron-common/startup-task';
export const WindowServiceExt = Symbol('WindowServiceExt');
export interface WindowServiceExt {
/**
* Returns with a promise that resolves to `true` if the current window is the first window.
*/
isFirstWindow(): Promise<boolean>;
reload(options?: StartupTask.Owner): void;
}

View File

@@ -1,44 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import {
SearchInWorkspaceFileNode,
SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget,
} from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { MEMORY_TEXT } from '@theia/core/lib/common/resource';
/**
* Workaround for https://github.com/eclipse-theia/theia/pull/9192/.
*/
@injectable()
export class SearchInWorkspaceResultTreeWidget extends TheiaSearchInWorkspaceResultTreeWidget {
protected override async createReplacePreview(
node: SearchInWorkspaceFileNode
): Promise<URI> {
const fileUri = new URI(node.fileUri).withScheme('file');
const openedEditor = this.editorManager.all.find(
({ editor }) => editor.uri.toString() === fileUri.toString()
);
let content: string;
if (openedEditor) {
content = openedEditor.editor.document.getText();
} else {
const resource = await this.fileResourceResolver.resolve(fileUri);
content = await resource.readContents();
}
const lines = content.split('\n');
node.children.map((l) => {
const leftPositionedNodes = node.children.filter(
(rl) => rl.line === l.line && rl.character < l.character
);
const diff =
(this._replaceTerm.length - this.searchTerm.length) *
leftPositionedNodes.length;
const start = lines[l.line - 1].substr(0, l.character - 1 + diff);
const end = lines[l.line - 1].substr(l.character - 1 + diff + l.length);
lines[l.line - 1] = start + this._replaceTerm + end;
});
return fileUri.withScheme(MEMORY_TEXT).withQuery(lines.join('\n'));
}
}

View File

@@ -1,80 +0,0 @@
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser';
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
/**
* Workaround for https://github.com/eclipse-theia/theia/pull/9183.
*/
@injectable()
export class SearchInWorkspaceWidget extends TheiaSearchInWorkspaceWidget {
@postConstruct()
protected override init(): void {
super.init();
this.title.iconClass = 'fa fa-arduino-search';
}
protected override renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
const currentValue = this.searchInWorkspaceOptions[kind];
const value = (currentValue && currentValue.join(', ')) || '';
return (
<div className="glob-field">
<div className="label">{'files to ' + kind}</div>
<input
className="theia-input"
type="text"
size={1}
defaultValue={value}
id={kind + '-glob-field'}
onKeyUp={(e) => {
if (e.target) {
const targetValue = (e.target as HTMLInputElement).value || '';
let shouldSearch =
Key.ENTER.keyCode ===
KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
const currentOptions = (this.searchInWorkspaceOptions[kind] || [])
.slice()
.map((s) => s.trim())
.sort();
const candidateOptions = this.splitOnComma(targetValue)
.map((s) => s.trim())
.sort();
const sameAs = (left: string[], right: string[]) => {
if (left.length !== right.length) {
return false;
}
for (let i = 0; i < left.length; i++) {
if (left[i] !== right[i]) {
return false;
}
}
return true;
};
if (!sameAs(currentOptions, candidateOptions)) {
this.searchInWorkspaceOptions[kind] =
this.splitOnComma(targetValue);
shouldSearch = true;
}
if (shouldSearch) {
this.resultTreeWidget.search(
this.searchTerm,
this.searchInWorkspaceOptions
);
}
}
}}
onFocus={
kind === 'include'
? this.handleFocusIncludesInputBox
: this.handleFocusExcludesInputBox
}
onBlur={
kind === 'include'
? this.handleBlurIncludesInputBox
: this.handleBlurExcludesInputBox
}
></input>
</div>
);
}
}

View File

@@ -17,7 +17,6 @@ import {
SketchesServiceClientImpl,
} from '../../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SingleTextInputDialog } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
@injectable()
@@ -161,20 +160,26 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
return;
}
const initialValue = uri.path.base;
const dialog = new SingleTextInputDialog({
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
initialValue,
initialSelectionRange: {
start: 0,
end: uri.path.name.length,
const parentUri = parent.resource;
const dialog = new WorkspaceInputDialog(
{
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
initialValue,
parentUri,
initialSelectionRange: {
start: 0,
end: uri.path.name.length,
},
validate: (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
},
},
validate: (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
},
});
this.labelProvider
);
const newName = await dialog.open();
const newNameWithExt = this.maybeAppendInoExt(newName);
if (newNameWithExt) {

View File

@@ -14,9 +14,11 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
constructor(
@inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider) protected override readonly labelProvider: LabelProvider
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider
) {
super(props, labelProvider);
this.node.classList.add('workspace-input-dialog');
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
@@ -41,4 +43,14 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
this.errorMessageNode.innerText = DialogError.getMessage(error);
}
}
protected override appendCloseButton(text: string): HTMLButtonElement {
this.closeButton = this.createButton(text);
this.controlPanel.insertBefore(
this.closeButton,
this.controlPanel.lastChild
);
this.closeButton.classList.add('secondary');
return this.closeButton;
}
}

View File

@@ -1,56 +1,45 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { injectable, inject } from '@theia/core/shared/inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
DEFAULT_WINDOW_HASH,
NewWindowOptions,
} from '@theia/core/lib/common/window';
import {
WorkspaceInput,
WorkspaceService as TheiaWorkspaceService,
} from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../../../common/protocol/config-service';
import {
SketchesService,
Sketch,
SketchesError,
} from '../../../common/protocol/sketches-service';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { BoardsConfig } from '../../boards/boards-config';
import { FileStat } from '@theia/filesystem/lib/common/files';
import {
StartupTask,
StartupTasks,
} from '../../widgets/sketchbook/startup-task';
import { setURL } from '../../utils/window';
StartupTaskProvider,
} from '../../../electron-common/startup-task';
import { WindowServiceExt } from '../core/window-service-ext';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
@injectable()
export class WorkspaceService extends TheiaWorkspaceService {
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider;
@inject(MessageService)
protected override readonly messageService: MessageService;
private readonly sketchService: SketchesService;
@inject(ApplicationServer)
protected readonly applicationServer: ApplicationServer;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
private readonly applicationServer: ApplicationServer;
@inject(WindowServiceExt)
private readonly windowServiceExt: WindowServiceExt;
@inject(ContributionProvider)
@named(StartupTaskProvider)
private readonly providers: ContributionProvider<StartupTaskProvider>;
private version?: string;
private _workspaceError: Error | undefined;
async onStart(application: FrontendApplication): Promise<void> {
const info = await this.applicationServer.getApplicationInfo();
@@ -64,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
this.onCurrentWidgetChange({ newValue, oldValue: null });
}
get workspaceError(): Error | undefined {
return this._workspaceError;
}
protected override async toFileStat(
uri: string | URI | undefined
): Promise<FileStat | undefined> {
@@ -72,6 +65,31 @@ export class WorkspaceService extends TheiaWorkspaceService {
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
}
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
// Nothing will work if the workspace file is invalid. Users tend to start (see #964) IDE2 from the `.ino` files,
// so here, IDE2 tries to load the sketch via the CLI from the main sketch file URI.
// If loading the sketch is OK, IDE2 starts and uses the sketch folder as the workspace root instead of the sketch file.
// If loading fails due to invalid name error, IDE2 loads a temp sketch and preserves the startup error, and offers the sketch move to the user later.
// If loading the sketch fails, create a fallback sketch and open the new temp sketch folder as the workspace root.
if (stat.isFile && stat.resource.path.ext === '.ino') {
try {
const sketch = await this.sketchService.loadSketch(
stat.resource.toString()
);
return this.toFileStat(sketch.uri);
} catch (err) {
if (SketchesError.InvalidName.is(err)) {
this._workspaceError = err;
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
} else if (SketchesError.NotFound.is(err)) {
this._workspaceError = err;
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
}
throw err;
}
}
return stat;
}
@@ -156,27 +174,33 @@ export class WorkspaceService extends TheiaWorkspaceService {
}
protected override reloadWindow(options?: WorkspaceInput): void {
if (StartupTasks.WorkspaceInput.is(options)) {
setURL(StartupTask.append(options.tasks, new URL(window.location.href)));
}
super.reloadWindow();
const tasks = this.tasks(options);
this.setURLFragment(this._workspace?.resource.path.toString() || '');
this.windowServiceExt.reload({ tasks });
}
protected override openNewWindow(
workspacePath: string,
options?: WorkspaceInput
): void {
const { boardsConfig } = this.boardsServiceProvider;
let url = BoardsConfig.Config.setConfig(
boardsConfig,
new URL(window.location.href)
); // Set the current boards config for the new browser window.
url.hash = workspacePath;
if (StartupTasks.WorkspaceInput.is(options)) {
url = StartupTask.append(options.tasks, url);
}
const tasks = this.tasks(options);
const url = new URL(window.location.href);
url.hash = encodeURI(workspacePath);
this.windowService.openNewWindow(
url.toString(),
Object.assign({} as NewWindowOptions, { tasks })
);
}
this.windowService.openNewWindow(url.toString());
private tasks(options?: WorkspaceInput): StartupTask[] {
const tasks = this.providers
.getContributions()
.map((contribution) => contribution.tasks())
.reduce((prev, curr) => prev.concat(curr), []);
if (StartupTask.has(options)) {
tasks.push(...options.tasks);
}
return tasks;
}
protected onCurrentWidgetChange({

View File

@@ -61,10 +61,10 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
</div>
</div>
<button
className="theia-button"
className="theia-button uppercase"
onClick={() => shell.openExternal('https://create.arduino.cc/editor')}
>
{nls.localize('cloud/GoToCloud', 'GO TO CLOUD')}
{nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')}
</button>
<div className="center item"></div>
</div>

View File

@@ -14,34 +14,21 @@ export class ComponentListItem<
)[0];
this.state = {
selectedVersion: version,
focus: false,
};
}
}
override componentDidUpdate(
prevProps: ComponentListItem.Props<T>,
prevState: ComponentListItem.State
): void {
if (this.state.focus !== prevState.focus) {
this.props.onFocusDidChange();
}
}
override render(): React.ReactNode {
const { item, itemRenderer } = this.props;
return (
<div
onMouseEnter={() => this.setState({ focus: true })}
onMouseLeave={() => this.setState({ focus: false })}
>
<>
{itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
)}
</div>
</>
);
}
@@ -77,11 +64,9 @@ export namespace ComponentListItem {
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
readonly onFocusDidChange: () => void;
}
export interface State {
selectedVersion?: Installable.Version;
focus: boolean;
}
}

View File

@@ -125,7 +125,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
rowIndex={index}
parent={parent}
>
{({ measure, registerChild }) => (
{({ registerChild }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<div ref={registerChild} style={style}>
@@ -135,7 +135,6 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall}
onFocusDidChange={() => measure()}
/>
</div>
)}

View File

@@ -111,19 +111,7 @@ export class FilterableListContainer<
const { searchable } = this.props;
searchable
.search(searchOptions)
.then((items) => this.setState({ items: this.sort(items) }));
}
protected sort(items: T[]): T[] {
const { itemLabel, itemDeprecated } = this.props;
return items.sort((left, right) => {
// always put deprecated items at the bottom of the list
if (itemDeprecated(left)) {
return 1;
}
return itemLabel(left).localeCompare(itemLabel(right));
});
.then((items) => this.setState({ items: this.props.sort(items) }));
}
protected async install(
@@ -139,7 +127,7 @@ export class FilterableListContainer<
run: ({ progressId }) => install({ item, progressId, version }),
});
const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.sort(items) });
this.setState({ items: this.props.sort(items) });
}
protected async uninstall(item: T): Promise<void> {
@@ -167,7 +155,7 @@ export class FilterableListContainer<
run: ({ progressId }) => uninstall({ item, progressId }),
});
const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.sort(items) });
this.setState({ items: this.props.sort(items) });
}
}
@@ -204,6 +192,7 @@ export namespace FilterableListContainer {
progressId: string;
}) => Promise<void>;
readonly commandService: CommandService;
readonly sort: (items: T[]) => T[];
}
export interface State<T, S extends Searchable.Options> {

View File

@@ -28,7 +28,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
const { item, focus } = input;
const { item } = input;
let nameAndAuthor: JSX.Element;
if (item.name && item.author) {
const name = <span className="name">{item.name}</span>;
@@ -55,7 +55,14 @@ export class ListItemRenderer<T extends ArduinoComponent> {
item.installedVersion
)}
</span>
<span className="installed" onClick={onClickUninstall} />
<span
className="installed uppercase"
onClick={onClickUninstall}
{...{
install: nls.localize('arduino/component/installed', 'Installed'),
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
}}
/>
</div>
);
@@ -70,10 +77,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
const onClickInstall = () => install(item);
const installButton = item.installable && (
<button
className="theia-button secondary install"
className="theia-button secondary install uppercase"
onClick={onClickInstall}
>
{nls.localize('arduino/component/install', 'INSTALL')}
{nls.localize('arduino/component/install', 'Install')}
</button>
);
@@ -120,12 +127,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
{description}
</div>
<div className="info">{moreInfo}</div>
{focus && (
<div className="footer">
{versions}
{installButton}
</div>
)}
<div className="footer">
{versions}
{installButton}
</div>
</div>
);
}

View File

@@ -1,9 +1,15 @@
import { injectable } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
OpenerOptions,
OpenHandler,
} from '@theia/core/lib/browser/opener-service';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { URI } from '@theia/core/lib/common/uri';
import { injectable } from '@theia/core/shared/inversify';
import { Searchable } from '../../../common/protocol';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget';
import { Searchable } from '../../../common/protocol';
@injectable()
export abstract class ListWidgetFrontendContribution<
@@ -11,14 +17,49 @@ export abstract class ListWidgetFrontendContribution<
S extends Searchable.Options
>
extends AbstractViewContribution<ListWidget<T, S>>
implements FrontendApplicationContribution
implements FrontendApplicationContribution, OpenHandler
{
readonly id: string = `http-opener-${this.viewId}`;
async initializeLayout(): Promise<void> {
// TS requires at least one method from `FrontendApplicationContribution`.
// Expected to be empty.
this.openView();
}
override registerMenus(): void {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override registerMenus(_: MenuModelRegistry): void {
// NOOP
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canHandle(uri: URI, _?: OpenerOptions): number {
// `500` is the default HTTP opener in Theia. IDE2 has higher priority.
// https://github.com/eclipse-theia/theia/blob/b75b6144b0ffea06a549294903c374fa642135e4/packages/core/src/browser/http-open-handler.ts#L39
return this.canParse(uri) ? 501 : 0;
}
async open(
uri: URI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_?: OpenerOptions | undefined
): Promise<void> {
const searchOptions = this.parse(uri);
if (!searchOptions) {
console.warn(
`Failed to parse URI into a search options. URI: ${uri.toString()}`
);
return;
}
const widget = await this.openView({
activate: true,
reveal: true,
});
if (!widget) {
console.warn(`Failed to open view for URI: ${uri.toString()}`);
return;
}
widget.refresh(searchOptions);
}
protected abstract canParse(uri: URI): boolean;
protected abstract parse(uri: URI): S | undefined;
}

View File

@@ -51,9 +51,11 @@ export abstract class ListWidget<
*/
protected firstActivate = true;
protected readonly defaultSortComparator: (left: T, right: T) => number;
constructor(protected options: ListWidget.Options<T, S>) {
super();
const { id, label, iconClass } = options;
const { id, label, iconClass, itemDeprecated, itemLabel } = options;
this.id = id;
this.title.label = label;
this.title.caption = label;
@@ -63,12 +65,23 @@ export abstract class ListWidget<
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = undefined;
this.toDispose.push(this.searchOptionsChangeEmitter);
this.defaultSortComparator = (left, right): number => {
// always put deprecated items at the bottom of the list
if (itemDeprecated(left)) {
return 1;
}
return itemLabel(left).localeCompare(itemLabel(right));
};
}
@postConstruct()
protected init(): void {
this.toDispose.pushAll([
this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)),
this.notificationCenter.onIndexUpdateDidComplete(() =>
this.refresh(undefined)
),
this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)),
this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)),
]);
@@ -128,6 +141,30 @@ export abstract class ListWidget<
return this.options.installable.uninstall({ item, progressId });
}
protected filterableListSort = (items: T[]): T[] => {
const isArduinoTypeComparator = (left: T, right: T) => {
const aIsArduinoType = left.types.includes('Arduino');
const bIsArduinoType = right.types.includes('Arduino');
if (aIsArduinoType && !bIsArduinoType && !left.deprecated) {
return -1;
}
if (!aIsArduinoType && bIsArduinoType && !right.deprecated) {
return 1;
}
return 0;
};
return items.sort((left, right) => {
return (
isArduinoTypeComparator(left, right) ||
this.defaultSortComparator(left, right)
);
});
};
render(): React.ReactNode {
return (
<FilterableListContainer<T, S>
@@ -145,6 +182,7 @@ export abstract class ListWidget<
messageService={this.messageService}
commandService={this.commandService}
responseService={this.responseService}
sort={this.filterableListSort}
/>
);
}

View File

@@ -1,92 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser';
import { Contribution } from '../../contributions/contribution';
import { setURL } from '../../utils/window';
@injectable()
export class StartupTasks extends Contribution {
override onReady(): void {
const tasks = StartupTask.get(new URL(window.location.href));
console.log(`Executing startup tasks: ${JSON.stringify(tasks)}`);
tasks.forEach(({ command, args = [] }) =>
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
)
);
if (tasks.length) {
// Remove the startup tasks after the execution.
// Otherwise, IDE2 executes them again on a window reload event.
setURL(StartupTask.set([], new URL(window.location.href)));
console.info(`Removed startup tasks from URL.`);
}
}
}
export interface StartupTask {
command: string;
/**
* Must be JSON serializable.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
}
export namespace StartupTask {
const QUERY = 'startupTasks';
export function is(arg: unknown): arg is StartupTasks {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return 'command' in object && typeof object['command'] === 'string';
}
return false;
}
export function get(url: URL): StartupTask[] {
const { searchParams } = url;
const encodedTasks = searchParams.get(QUERY);
if (encodedTasks) {
const rawTasks = decodeURIComponent(encodedTasks);
const tasks = JSON.parse(rawTasks);
if (Array.isArray(tasks)) {
return tasks.filter((task) => {
if (StartupTask.is(task)) {
return true;
}
console.warn(`Was not a task: ${JSON.stringify(task)}. Ignoring.`);
return false;
});
} else {
debugger;
console.warn(`Startup tasks was not an array: ${rawTasks}. Ignoring.`);
}
}
return [];
}
export function set(tasks: StartupTask[], url: URL): URL {
const copy = new URL(url);
copy.searchParams.set(QUERY, encodeURIComponent(JSON.stringify(tasks)));
return copy;
}
export function append(tasks: StartupTask[], url: URL): URL {
return set([...get(url), ...tasks], url);
}
}
export namespace StartupTasks {
export interface WorkspaceInput extends TheiaWorkspaceInput {
tasks: StartupTask[];
}
export namespace WorkspaceInput {
export function is(
input: (TheiaWorkspaceInput & Partial<WorkspaceInput>) | undefined
): input is WorkspaceInput {
return !!input && !!input.tasks;
}
}
}

View File

@@ -0,0 +1,2 @@
export const SHOW_PLOTTER_WINDOW = 'SHOW_PLOTTER_WINDOW';
export const CLOSE_PLOTTER_WINDOW = 'CLOSE_PLOTTER_WINDOW';

View File

@@ -3,7 +3,14 @@ import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
import { nls } from '@theia/core/lib/common/nls';
import { All, Contributed, Partner, Type, Updatable } from '../nls';
import {
All,
Contributed,
Partner,
Type as TypeLabel,
Updatable,
} from '../nls';
import URI from '@theia/core/lib/common/uri';
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
export namespace AvailablePorts {
@@ -141,6 +148,7 @@ export interface BoardsService
fqbn: string;
}): Promise<BoardsPackage | undefined>;
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
getInstalledBoards(): Promise<BoardWithPackage[]>;
getBoardUserFields(options: {
fqbn: string;
protocol: string;
@@ -151,6 +159,7 @@ export interface BoardSearch extends Searchable.Options {
readonly type?: BoardSearch.Type;
}
export namespace BoardSearch {
export const Default: BoardSearch = { type: 'All' };
export const TypeLiterals = [
'All',
'Updatable',
@@ -161,6 +170,11 @@ export namespace BoardSearch {
'Arduino@Heart',
] as const;
export type Type = typeof TypeLiterals[number];
export namespace Type {
export function is(arg: unknown): arg is Type {
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
}
}
export const TypeLabels: Record<Type, string> = {
All: All,
Updatable: Updatable,
@@ -177,8 +191,41 @@ export namespace BoardSearch {
keyof Omit<BoardSearch, 'query'>,
string
> = {
type: Type,
type: TypeLabel,
};
export namespace UriParser {
export const authority = 'boardsmanager';
export function parse(uri: URI): BoardSearch | undefined {
if (uri.scheme !== 'http') {
throw new Error(
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
);
}
if (uri.authority !== authority) {
throw new Error(
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
);
}
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
if (segments.length !== 1) {
return undefined;
}
let searchOptions: BoardSearch | undefined = undefined;
const [type] = segments;
if (!type) {
searchOptions = BoardSearch.Default;
} else if (BoardSearch.Type.is(type)) {
searchOptions = { type };
}
if (searchOptions) {
return {
...searchOptions,
...Searchable.UriParser.parseQuery(uri),
};
}
return undefined;
}
}
}
export interface Port {
@@ -285,6 +332,29 @@ export namespace Port {
return false;
};
}
export namespace Protocols {
export const KnownProtocolLiterals = ['serial', 'network'] as const;
export type KnownProtocol = typeof KnownProtocolLiterals[number];
export namespace KnownProtocol {
export function is(protocol: unknown): protocol is KnownProtocol {
return (
typeof protocol === 'string' &&
KnownProtocolLiterals.indexOf(protocol as KnownProtocol) >= 0
);
}
}
export const ProtocolLabels: Record<KnownProtocol, string> = {
serial: nls.localize('arduino/portProtocol/serial', 'Serial'),
network: nls.localize('arduino/portProtocol/network', 'Network'),
};
export function protocolLabel(protocol: string): string {
if (KnownProtocol.is(protocol)) {
return ProtocolLabels[protocol];
}
return protocol;
}
}
}
export interface BoardsPackage extends ArduinoComponent {

View File

@@ -1,3 +1,4 @@
import { nls } from '@theia/core/lib/common/nls';
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type {
Location,
@@ -10,6 +11,7 @@ import type {
} from '../../common/protocol/boards-service';
import type { Programmer } from './boards-service';
import type { Sketch } from './sketches-service';
import { IndexUpdateSummary } from './notification-service';
export const CompilerWarningLiterals = [
'None',
@@ -18,6 +20,17 @@ export const CompilerWarningLiterals = [
'All',
] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
export namespace CompilerWarnings {
export function labelOf(warning: CompilerWarnings): string {
return CompilerWarningLabels[warning];
}
const CompilerWarningLabels: Record<CompilerWarnings, string> = {
None: nls.localize('arduino/core/compilerWarnings/none', 'None'),
Default: nls.localize('arduino/core/compilerWarnings/default', 'Default'),
More: nls.localize('arduino/core/compilerWarnings/more', 'More'),
All: nls.localize('arduino/core/compilerWarnings/all', 'All'),
};
}
export namespace CoreError {
export interface ErrorLocationRef {
readonly message: string;
@@ -96,6 +109,37 @@ export interface CoreService {
compile(options: CoreService.Options.Compile): Promise<void>;
upload(options: CoreService.Options.Upload): Promise<void>;
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
/**
* Refreshes the underling core gRPC client for the Arduino CLI.
*/
refresh(): Promise<void>;
/**
* Updates the index of the given index types and refreshes (`init`) the underlying core gRPC client.
* If `types` is empty, only the refresh part will be executed.
*/
updateIndex({ types }: { types: IndexType[] }): Promise<void>;
/**
* If the IDE2 detects invalid or missing indexes on core client init,
* IDE2 tries to update the indexes before the first frontend connects.
* Use this method to determine whether the backend has already updated
* the indexes before updating them.
*
* If yes, the connected frontend can update the local storage with the most
* recent index update date-time for a particular index type,
* and IDE2 can avoid the double indexes update.
*/
indexUpdateSummaryBeforeInit(): Promise<Readonly<IndexUpdateSummary>>;
}
export const IndexTypeLiterals = ['platform', 'library'] as const;
export type IndexType = typeof IndexTypeLiterals[number];
export namespace IndexType {
export function is(arg: unknown): arg is IndexType {
return (
typeof arg === 'string' && IndexTypeLiterals.includes(arg as IndexType)
);
}
export const All: IndexType[] = IndexTypeLiterals.filter(is);
}
export namespace CoreService {

View File

@@ -8,9 +8,10 @@ import {
Partner,
Recommended,
Retired,
Type,
Type as TypeLabel,
Updatable,
} from '../nls';
import URI from '@theia/core/lib/common/uri';
export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService');
@@ -55,6 +56,7 @@ export interface LibrarySearch extends Searchable.Options {
readonly topic?: LibrarySearch.Topic;
}
export namespace LibrarySearch {
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
export const TypeLiterals = [
'All',
'Updatable',
@@ -66,6 +68,11 @@ export namespace LibrarySearch {
'Retired',
] as const;
export type Type = typeof TypeLiterals[number];
export namespace Type {
export function is(arg: unknown): arg is Type {
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
}
}
export const TypeLabels: Record<Type, string> = {
All: All,
Updatable: Updatable,
@@ -90,6 +97,11 @@ export namespace LibrarySearch {
'Uncategorized',
] as const;
export type Topic = typeof TopicLiterals[number];
export namespace Topic {
export function is(arg: unknown): arg is Topic {
return typeof arg === 'string' && TopicLiterals.includes(arg as Topic);
}
}
export const TopicLabels: Record<Topic, string> = {
All: All,
Communication: nls.localize(
@@ -126,8 +138,60 @@ export namespace LibrarySearch {
string
> = {
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
type: Type,
type: TypeLabel,
};
export namespace UriParser {
export const authority = 'librarymanager';
export function parse(uri: URI): LibrarySearch | undefined {
if (uri.scheme !== 'http') {
throw new Error(
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
);
}
if (uri.authority !== authority) {
throw new Error(
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
);
}
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
// Special magic handling for `Signal Input/Output`.
// TODO: IDE2 deserves a better lib/boards URL spec.
// https://github.com/arduino/arduino-ide/issues/1442#issuecomment-1252136377
if (segments.length === 3) {
const [type, topicHead, topicTail] = segments;
const maybeTopic = `${topicHead}/${topicTail}`;
if (
LibrarySearch.Topic.is(maybeTopic) &&
maybeTopic === 'Signal Input/Output' &&
LibrarySearch.Type.is(type)
) {
return {
type,
topic: maybeTopic,
...Searchable.UriParser.parseQuery(uri),
};
}
}
let searchOptions: LibrarySearch | undefined = undefined;
const [type, topic] = segments;
if (!type && !topic) {
searchOptions = LibrarySearch.Default;
} else if (LibrarySearch.Type.is(type)) {
if (!topic) {
searchOptions = { ...LibrarySearch.Default, type };
} else if (LibrarySearch.Topic.is(topic)) {
searchOptions = { type, topic };
}
}
if (searchOptions) {
return {
...searchOptions,
...Searchable.UriParser.parseQuery(uri),
};
}
return undefined;
}
}
}
export namespace LibraryService {
@@ -162,12 +226,6 @@ export enum LibraryLocation {
}
export interface LibraryPackage extends ArduinoComponent {
/**
* Same as [`Library#real_name`](https://arduino.github.io/arduino-cli/latest/rpc/commands/#library).
* Should be used for the UI, and `name` is used to uniquely identify a library. It does not have an ID.
*/
readonly label: string;
/**
* An array of string that should be included into the `ino` file if this library is used.
* For example, including `SD` will prepend `#include <SD.h>` to the `ino` file. While including `Bridge`

View File

@@ -5,27 +5,62 @@ import type {
Config,
ProgressMessage,
Sketch,
IndexType,
} from '../protocol';
import type { LibraryPackage } from './library-service';
/**
* Values are [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
* strings representing the date-time when the update of the index has been completed.
*/
export type IndexUpdateSummary = {
[T in IndexType]: string;
} & { message?: string };
export interface IndexUpdateParams {
/**
* Application unique ID of the progress.
*/
readonly progressId: string;
/**
* The type of the index is which is being updated.
*/
readonly types: IndexType[];
}
export type IndexUpdateWillStartParams = IndexUpdateParams;
export interface IndexUpdateDidCompleteParams
extends Omit<IndexUpdateParams, 'types'> {
readonly summary: IndexUpdateSummary;
}
export interface IndexUpdateDidFailParams extends IndexUpdateParams {
/**
* Describes the reason of the index update failure.
*/
readonly message: string;
}
export interface NotificationServiceClient {
notifyIndexWillUpdate(progressId: string): void;
// Index
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
notifyIndexDidUpdate(progressId: string): void;
notifyIndexUpdateDidFail({
progressId,
message,
}: {
progressId: string;
message: string;
}): void;
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void;
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void;
// Daemon
notifyDaemonDidStart(port: string): void;
notifyDaemonDidStop(): void;
// CLI config
notifyConfigDidChange(event: { config: Config | undefined }): void;
// Platforms
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void;
// Libraries
notifyLibraryDidInstall(event: { item: LibraryPackage }): void;
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void;
// Boards discovery
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void;
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void;
}

View File

@@ -1,3 +1,5 @@
import URI from '@theia/core/lib/common/uri';
export interface Searchable<T, O extends Searchable.Options> {
search(options: O): Promise<T[]>;
}
@@ -8,4 +10,24 @@ export namespace Searchable {
*/
readonly query?: string;
}
export namespace UriParser {
/**
* Parses the `URI#fragment` into a query term.
*/
export function parseQuery(uri: URI): { query: string } {
return { query: uri.fragment };
}
/**
* Splits the `URI#path#toString` on the `/` POSIX separator into decoded segments. The first, empty segment representing the root is omitted.
* Examples:
* - `/` -> `['']`
* - `/All` -> `['All']`
* - `/All/Device%20Control` -> `['All', 'Device Control']`
* - `/All/Display` -> `['All', 'Display']`
* - `/Updatable/Signal%20Input%2FOutput` -> `['Updatable', 'Signal Input', 'Output']` (**caveat**!)
*/
export function normalizedSegmentsOf(uri: URI): string[] {
return uri.path.toString().split('/').slice(1).map(decodeURIComponent);
}
}
}

View File

@@ -165,15 +165,6 @@ export class SketchesServiceClientImpl
.reachedState('started_contributions')
.then(async () => {
const currentSketch = await this.loadCurrentSketch();
if (CurrentSketch.isValid(currentSketch)) {
this.toDispose.pushAll([
// Watch the file changes of the current sketch
this.fileService.watch(new URI(currentSketch.uri), {
recursive: true,
excludes: [],
}),
]);
}
this.useCurrentSketch(currentSketch);
});
}

View File

@@ -4,6 +4,7 @@ import URI from '@theia/core/lib/common/uri';
export namespace SketchesError {
export const Codes = {
NotFound: 5001,
InvalidName: 5002,
};
export const NotFound = ApplicationError.declare(
Codes.NotFound,
@@ -14,6 +15,15 @@ export namespace SketchesError {
};
}
);
export const InvalidName = ApplicationError.declare(
Codes.InvalidName,
(message: string, invalidMainSketchUri: string) => {
return {
message,
data: { invalidMainSketchUri },
};
}
);
}
export const SketchesServicePath = '/services/sketches-service';
@@ -21,16 +31,9 @@ export const SketchesService = Symbol('SketchesService');
export interface SketchesService {
/**
* Resolves to a sketch container representing the hierarchical structure of the sketches.
* If `uri` is not given, `directories.user` will be user instead. Specify `exclude` global patterns to filter folders from the sketch container.
* If `exclude` is not set `['**\/libraries\/**', '**\/hardware\/**']` will be used instead.
* If `uri` is not given, `directories.user` will be user instead.
*/
getSketches({
uri,
exclude,
}: {
uri?: string;
exclude?: string[];
}): Promise<SketchContainer>;
getSketches({ uri }: { uri?: string }): Promise<SketchContainer>;
/**
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
@@ -71,7 +74,7 @@ export interface SketchesService {
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
/**
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, resolved `undefined`.
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.
*/
getSketchFolder(uri: string): Promise<Sketch | undefined>;
@@ -82,8 +85,10 @@ export interface SketchesService {
/**
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
* If `forceUpdate` is `true`, the array of recently opened sketches will be recalculated.
* Invalid and missing sketches will be removed from the list. It's `false` by default.
*/
recentlyOpenedSketches(): Promise<Sketch[]>;
recentlyOpenedSketches(forceUpdate?: boolean): Promise<Sketch[]>;
/**
* Archives the sketch, resolves to the archive URI.
@@ -97,9 +102,9 @@ export interface SketchesService {
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
/**
* Notifies the backend to recursively delete the sketch folder with all its content.
* Recursively deletes the sketch folder with all its content.
*/
notifyDeleteSketch(sketch: Sketch): void;
deleteSketch(sketch: Sketch): Promise<void>;
}
export interface SketchRef {
@@ -114,6 +119,19 @@ export namespace SketchRef {
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
};
}
export function is(arg: unknown): arg is SketchRef {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'name' in object &&
typeof object['name'] === 'string' &&
'uri' in object &&
typeof object['name'] === 'string'
);
}
return false;
}
}
export interface Sketch extends SketchRef {
readonly mainFileUri: string; // `MainFile`
@@ -122,14 +140,25 @@ export interface Sketch extends SketchRef {
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
}
export namespace Sketch {
export function is(arg: any): arg is Sketch {
return (
!!arg &&
'name' in arg &&
'uri' in arg &&
typeof arg.name === 'string' &&
typeof arg.uri === 'string'
);
export function is(arg: unknown): arg is Sketch {
if (!SketchRef.is(arg)) {
return false;
}
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'mainFileUri' in object &&
typeof object['mainFileUri'] === 'string' &&
'otherSketchFileUris' in object &&
Array.isArray(object['otherSketchFileUris']) &&
'additionalFileUris' in object &&
Array.isArray(object['additionalFileUris']) &&
'rootFolderFileUris' in object &&
Array.isArray(object['rootFolderFileUris'])
);
}
return false;
}
export namespace Extensions {
export const MAIN = ['.ino', '.pde'];

View File

@@ -1,10 +1,14 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import {
ConnectionStatus,
ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { NewWindowOptions } from '@theia/core/lib/common/window';
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service';
import { RELOAD_REQUESTED_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages';
import {
inject,
injectable,
@@ -12,6 +16,7 @@ import {
} from '@theia/core/shared/inversify';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext';
import { StartupTask } from '../../../electron-common/startup-task';
@injectable()
export class ElectronWindowService
@@ -60,14 +65,30 @@ export class ElectronWindowService
return response === 0; // 'Yes', close the window.
}
private _firstWindow: boolean | undefined;
private _firstWindow: Deferred<boolean> | undefined;
async isFirstWindow(): Promise<boolean> {
if (this._firstWindow === undefined) {
this._firstWindow = new Deferred<boolean>();
const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE.
this._firstWindow = await this.mainWindowServiceExt.isFirstWindow(
windowId
);
this.mainWindowServiceExt
.isFirstWindow(windowId)
.then((firstWindow) => this._firstWindow?.resolve(firstWindow));
}
return this._firstWindow.promise;
}
// Overridden because the default Theia implementation destroys the additional properties of the `options` arg, such as `tasks`.
override openNewWindow(url: string, options?: NewWindowOptions): undefined {
return this.delegate.openNewWindow(url, options);
}
// Overridden to support optional task owner params and make `tsc` happy.
override reload(options?: StartupTask.Owner): void {
if (options?.tasks && options.tasks.length) {
const { tasks } = options;
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL, { tasks });
} else {
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL);
}
return this._firstWindow;
}
}

View File

@@ -0,0 +1,50 @@
export interface StartupTask {
command: string;
/**
* Must be JSON serializable.
* See the restrictions [here](https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
}
export namespace StartupTask {
export function is(arg: unknown): arg is StartupTask {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'command' in object &&
typeof object['command'] === 'string' &&
(!('args' in object) || Array.isArray(object['args']))
);
}
return false;
}
export function has(arg: unknown): arg is unknown & Owner {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'tasks' in object &&
Array.isArray(object['tasks']) &&
object['tasks'].every(is)
);
}
return false;
}
export namespace Messaging {
export const STARTUP_TASKS_SIGNAL = 'arduino/startupTasks';
export function APP_READY_SIGNAL(id: number): string {
return `arduino/appReady${id}`;
}
}
export interface Owner {
readonly tasks: StartupTask[];
}
}
export const StartupTaskProvider = Symbol('StartupTaskProvider');
export interface StartupTaskProvider {
tasks(): StartupTask[];
}

View File

@@ -12,16 +12,13 @@ import {
IDEUpdaterClient,
IDEUpdaterPath,
} from '../common/protocol/ide-updater';
import {
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../electron-common/electron-main-window-service-ext';
import { electronMainWindowServiceExtPath } from '../electron-common/electron-main-window-service-ext';
import { IsTempSketch } from '../node/is-temp-sketch';
import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl';
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
import { TheiaElectronWindow } from './theia/theia-electron-window';
import { ElectronNativeKeymap } from '@theia/core/lib/electron-main/electron-native-keymap';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
@@ -52,17 +49,18 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
bind(ElectronMainWindowServiceExt)
.to(ElectronMainWindowServiceExtImpl)
.inSingletonScope();
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () =>
context.container.get(ElectronMainWindowServiceExt)
context.container.get(ElectronMainWindowServiceImpl)
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
// https://github.com/eclipse-theia/theia/issues/11688
bind(ElectronNativeKeymap).toSelf().inSingletonScope();
bind(ElectronMainApplicationContribution).toService(ElectronNativeKeymap);
});

View File

@@ -1,15 +0,0 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { ElectronMainWindowServiceExt } from '../electron-common/electron-main-window-service-ext';
import { ElectronMainApplication } from './theia/electron-main-application';
@injectable()
export class ElectronMainWindowServiceExtImpl
implements ElectronMainWindowServiceExt
{
@inject(ElectronMainApplication)
private readonly app: ElectronMainApplication;
async isFirstWindow(windowId: number): Promise<boolean> {
return this.app.firstWindowId === windowId;
}
}

View File

@@ -8,8 +8,8 @@ import {
} from '@theia/core/electron-shared/electron';
import { fork } from 'child_process';
import { AddressInfo } from 'net';
import { join, dirname } from 'path';
import * as fs from 'fs-extra';
import { join, isAbsolute, resolve } from 'path';
import { promises as fs, Stats } from 'fs';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
@@ -23,6 +23,11 @@ import * as os from '@theia/core/lib/common/os';
import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages';
import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
import { IsTempSketch } from '../../node/is-temp-sketch';
import {
CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW,
} from '../../common/ipc-communication';
import isValidPath = require('is-valid-path');
app.commandLine.appendSwitch('disable-http-cache');
@@ -65,8 +70,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
// See: https://github.com/electron-userland/electron-builder/issues/2468
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
console.log(`${config.applicationName} ${app.getVersion()}`);
app.on('ready', () => app.setName(config.applicationName));
this.attachFileAssociations();
const cwd = process.cwd();
this.attachFileAssociations(cwd);
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
@@ -80,7 +87,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
return this.launch({
secondInstance: false,
argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
cwd: process.cwd(),
cwd,
});
}
@@ -115,7 +122,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
let traceFile: string | undefined;
if (appPath) {
const tracesPath = join(appPath, 'traces');
await fs.promises.mkdir(tracesPath, { recursive: true });
await fs.mkdir(tracesPath, { recursive: true });
traceFile = join(tracesPath, `trace-${new Date().toISOString()}.trace`);
}
console.log('>>> Content tracing has started...');
@@ -131,14 +138,18 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
})();
}
private attachFileAssociations(): void {
private attachFileAssociations(cwd: string): void {
// OSX: register open-file event
if (os.isOSX) {
app.on('open-file', async (event, uri) => {
app.on('open-file', async (event, path) => {
event.preventDefault();
if (uri.endsWith('.ino') && (await fs.pathExists(uri))) {
this.openFilePromise.reject();
await this.openSketch(dirname(uri));
const resolvedPath = await this.resolvePath(path, cwd);
if (resolvedPath) {
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
if (sketchFolderPath) {
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
await this.openSketch(sketchFolderPath);
}
}
});
setTimeout(() => this.openFilePromise.resolve(), 500);
@@ -147,8 +158,68 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
private async isValidSketchPath(uri: string): Promise<boolean | undefined> {
return typeof uri === 'string' && (await fs.pathExists(uri));
/**
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
* or it's a directory, and one of the files in the directory is an `.ino` file.
*
* If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
*
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
* The `path` must be an absolute, resolved path.
*/
private async isValidSketchPath(path: string): Promise<string | undefined> {
let stats: Stats | undefined = undefined;
try {
stats = await fs.stat(path);
} catch (err) {
if ('code' in err && err.code === 'ENOENT') {
return undefined;
}
throw err;
}
if (!stats) {
return undefined;
}
if (stats.isFile() && path.endsWith('.ino')) {
return path;
}
try {
const entries = await fs.readdir(path, { withFileTypes: true });
const sketchFilename = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
.map(({ name }) => name)
.sort((left, right) => left.localeCompare(right))[0];
if (sketchFilename) {
return join(path, sketchFilename);
}
// If no sketches found in the folder, but the folder exists,
// return with the path of the empty folder and let IDE2's frontend
// figure out the workspace root.
return path;
} catch (err) {
throw err;
}
}
private async resolvePath(
maybePath: string,
cwd: string
): Promise<string | undefined> {
if (!isValidPath(maybePath)) {
return undefined;
}
if (isAbsolute(maybePath)) {
return maybePath;
}
try {
const resolved = await fs.realpath(resolve(cwd, maybePath));
return resolved;
} catch (err) {
if ('code' in err && err.code === 'ENOENT') {
return undefined;
}
throw err;
}
}
protected override async launch(
@@ -159,12 +230,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
// 1. The `open-file` command has been received by the app, rejecting the promise
// 2. A short timeout resolves the promise automatically, falling back to the usual app launch
await this.openFilePromise.promise;
} catch {
// Application has received the `open-file` event and will skip the default application launch
return;
} catch (err) {
if (err instanceof InterruptWorkspaceRestoreError) {
// Application has received the `open-file` event and will skip the default application launch
return;
}
throw err;
}
if (!os.isOSX && (await this.launchFromArgs(params))) {
if (await this.launchFromArgs(params)) {
// Application has received a file in its arguments and will skip the default application launch
return;
}
@@ -178,7 +252,13 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
);
for (const workspace of workspaces) {
if (await this.isValidSketchPath(workspace.file)) {
const resolvedPath = await this.resolvePath(workspace.file, params.cwd);
if (!resolvedPath) {
continue;
}
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
if (sketchFolderPath) {
workspace.file = sketchFolderPath;
if (this.isTempSketch.is(workspace.file)) {
console.info(
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
@@ -201,38 +281,40 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
): Promise<boolean> {
// Copy to prevent manipulation of original array
const argCopy = [...params.argv];
let uri: string | undefined;
for (const possibleUri of argCopy) {
if (
possibleUri.endsWith('.ino') &&
(await this.isValidSketchPath(possibleUri))
) {
uri = possibleUri;
let path: string | undefined;
for (const maybePath of argCopy) {
const resolvedPath = await this.resolvePath(maybePath, params.cwd);
if (!resolvedPath) {
continue;
}
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
if (sketchFolderPath) {
path = sketchFolderPath;
break;
}
}
if (uri) {
await this.openSketch(dirname(uri));
if (path) {
await this.openSketch(path);
return true;
}
return false;
}
private async openSketch(
workspace: WorkspaceOptions | string
workspaceOrPath: WorkspaceOptions | string
): Promise<BrowserWindow> {
const options = await this.getLastWindowOptions();
let file: string;
if (typeof workspace === 'object') {
options.x = workspace.x;
options.y = workspace.y;
options.width = workspace.width;
options.height = workspace.height;
options.isMaximized = workspace.isMaximized;
options.isFullScreen = workspace.isFullScreen;
file = workspace.file;
if (typeof workspaceOrPath === 'object') {
options.x = workspaceOrPath.x;
options.y = workspaceOrPath.y;
options.width = workspaceOrPath.width;
options.height = workspaceOrPath.height;
options.isMaximized = workspaceOrPath.isMaximized;
options.isFullScreen = workspaceOrPath.isFullScreen;
file = workspaceOrPath.file;
} else {
file = workspace;
file = workspaceOrPath;
}
const [uri, electronWindow] = await Promise.all([
this.createWindowUri(),
@@ -324,15 +406,21 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
webPreferences: {
devTools: true,
nativeWindowOpen: true,
openerId: electronWindow?.webContents.id,
openerId: electronWindow.webContents.id,
},
});
event.newGuest = new BrowserWindow(options);
const showPlotterWindow = () => {
event.newGuest?.show();
};
ipcMain.on(SHOW_PLOTTER_WINDOW, showPlotterWindow);
event.newGuest.setMenu(null);
event.newGuest?.on('closed', () => {
electronWindow?.webContents.send('CLOSE_CHILD_WINDOW');
event.newGuest.on('closed', () => {
ipcMain.removeListener(SHOW_PLOTTER_WINDOW, showPlotterWindow);
electronWindow.webContents.send(CLOSE_PLOTTER_WINDOW);
});
event.newGuest?.loadURL(url);
event.newGuest.loadURL(url);
}
}
);
@@ -476,3 +564,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
return this._firstWindowId;
}
}
class InterruptWorkspaceRestoreError extends Error {
constructor() {
super(
"Received 'open-file' event. Interrupting the default launch workflow."
);
Object.setPrototypeOf(this, InterruptWorkspaceRestoreError.prototype);
}
}

View File

@@ -1,34 +1,63 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import type { NewWindowOptions } from '@theia/core/lib/common/window';
import type { BrowserWindow } from '@theia/core/electron-shared/electron';
import { ElectronMainWindowServiceImpl as TheiaElectronMainWindowService } from '@theia/core/lib/electron-main/electron-main-window-service-impl';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ElectronMainWindowServiceExt } from '../../electron-common/electron-main-window-service-ext';
import { StartupTask } from '../../electron-common/startup-task';
import { ElectronMainApplication } from './electron-main-application';
import { NewWindowOptions } from '@theia/core/lib/common/window';
import { load } from './window';
@injectable()
export class ElectronMainWindowServiceImpl extends TheiaElectronMainWindowService {
export class ElectronMainWindowServiceImpl
extends TheiaElectronMainWindowService
implements ElectronMainWindowServiceExt
{
@inject(ElectronMainApplication)
protected override readonly app: ElectronMainApplication;
override openNewWindow(url: string, { external }: NewWindowOptions): undefined {
if (!external) {
const sanitizedUrl = this.sanitize(url);
const existing = this.app.browserWindows.find(
(window) => this.sanitize(window.webContents.getURL()) === sanitizedUrl
);
if (existing) {
existing.focus();
return;
}
}
return super.openNewWindow(url, { external });
async isFirstWindow(windowId: number): Promise<boolean> {
return this.app.firstWindowId === windowId;
}
private sanitize(url: string): string {
const copy = new URL(url);
const searchParams: string[] = [];
copy.searchParams.forEach((_, key) => searchParams.push(key));
for (const param of searchParams) {
copy.searchParams.delete(param);
override openNewWindow(url: string, options: NewWindowOptions): undefined {
// External window has highest precedence.
if (options?.external) {
return super.openNewWindow(url, options);
}
return copy.toString();
// Look for existing window with the same URL and focus it.
const existing = this.app.browserWindows.find(
({ webContents }) => webContents.getURL() === url
);
if (existing) {
existing.focus();
return undefined;
}
// Create new window and share the startup tasks.
if (StartupTask.has(options)) {
const { tasks } = options;
this.app.createWindow().then((electronWindow) => {
this.loadURL(electronWindow, url).then(() => {
electronWindow.webContents.send(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
{ tasks }
);
});
});
return undefined;
}
// Default.
return super.openNewWindow(url, options);
}
private loadURL(
electronWindow: BrowserWindow,
url: string
): Promise<BrowserWindow> {
return load(electronWindow, (electronWindow) =>
electronWindow.loadURL(url)
);
}
}

View File

@@ -1,12 +1,15 @@
import { injectable } from '@theia/core/shared/inversify';
import { ipcMain, IpcMainEvent } from '@theia/electron/shared/electron';
import { StopReason } from '@theia/core/lib/electron-common/messaging/electron-messages';
import {
RELOAD_REQUESTED_SIGNAL,
StopReason,
} from '@theia/core/lib/electron-common/messaging/electron-messages';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { FileUri } from '@theia/core/lib/node';
import URI from '@theia/core/lib/common/uri';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
import { APPLICATION_STATE_CHANGE_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages';
import { StartupTask } from '../../electron-common/startup-task';
import { load } from './window';
@injectable()
export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
@@ -38,30 +41,42 @@ export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
return false;
}
// Note: does the same as the Theia impl, but logs state changes.
protected override trackApplicationState(): void {
protected override reload(tasks?: StartupTask[]): void {
this.handleStopRequest(() => {
this.applicationState = 'init';
if (tasks && tasks.length) {
load(this._window, (electronWindow) => electronWindow.reload()).then(
(electronWindow) =>
electronWindow.webContents.send(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
{ tasks }
)
);
} else {
this._window.reload();
}
}, StopReason.Reload);
}
protected override attachReloadListener(): void {
createDisposableListener(
ipcMain,
APPLICATION_STATE_CHANGE_SIGNAL,
(e: IpcMainEvent, state: FrontendApplicationState) => {
console.log(
'app-state-change',
`>>> new app state <${state} was received from sender <${e.sender.id}>. current window ID: ${this._window.id}`
);
RELOAD_REQUESTED_SIGNAL,
(e: IpcMainEvent, arg: unknown) => {
if (this.isSender(e)) {
this.applicationState = state;
console.log(
'app-state-change',
`<<< new app state is <${this.applicationState}> for window <${this._window.id}>`
);
} else {
console.log(
'app-state-change',
`<<< new app state <${state}> is ignored from <${e.sender.id}>. current window ID is <${this._window.id}>`
);
if (StartupTask.has(arg)) {
this.reload(arg.tasks);
} else {
this.reload();
}
}
},
this.toDispose
);
}
// https://github.com/eclipse-theia/theia/issues/11600#issuecomment-1240657481
protected override isSender(e: IpcMainEvent): boolean {
return e.sender.id === this._window.webContents.id;
}
}

View File

@@ -0,0 +1,33 @@
import { MaybePromise } from '@theia/core';
import type { IpcMainEvent } from '@theia/core/electron-shared/electron';
import { BrowserWindow, ipcMain } from '@theia/core/electron-shared/electron';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
import { StartupTask } from '../../electron-common/startup-task';
/**
* Should be used to load (URL) or reload a window. The returning promise will resolve
* when the app is ready to receive startup tasks.
*/
export async function load(
electronWindow: BrowserWindow,
doLoad: (electronWindow: BrowserWindow) => MaybePromise<void>
): Promise<BrowserWindow> {
const { id } = electronWindow;
const toDispose = new DisposableCollection();
const channel = StartupTask.Messaging.APP_READY_SIGNAL(id);
return new Promise<BrowserWindow>((resolve, reject) => {
toDispose.push(
createDisposableListener(
ipcMain,
channel,
({ sender: webContents }: IpcMainEvent) => {
if (webContents.id === electronWindow.webContents.id) {
resolve(electronWindow);
}
}
)
);
Promise.resolve(doLoad(electronWindow)).catch(reject);
}).finally(() => toDispose.dispose());
}

View File

@@ -162,8 +162,6 @@ export class ArduinoDaemonImpl
'--config-file',
`"${cliConfigPath}"`,
'-v',
'--log-format',
'json',
];
if (debug) {
args.push('--debug');

View File

@@ -1,10 +1,9 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import {
ArduinoFirmwareUploader,
ArduinoFirmwareUploaderPath,
} from '../common/protocol/arduino-firmware-uploader';
import { ILogger } from '@theia/core/lib/common/logger';
import {
BackendApplicationContribution,
@@ -26,7 +25,7 @@ import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connec
import { CoreClientProvider } from './core-client-provider';
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { SketchesServiceImpl } from './sketches-service-impl';
import {
SketchesService,
@@ -40,7 +39,6 @@ import {
ArduinoDaemon,
ArduinoDaemonPath,
} from '../common/protocol/arduino-daemon';
import { ConfigServiceImpl } from './config-service-impl';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
@@ -110,6 +108,7 @@ import {
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch';
import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-watcher/nsfw-bindings';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@@ -288,6 +287,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
rebindNsfwFileSystemWatcher(rebind);
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(
@@ -325,58 +325,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
);
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('daemon');
})
.inSingletonScope()
.whenTargetNamed('daemon');
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('fwuploader');
})
.inSingletonScope()
.whenTargetNamed('fwuploader');
// Logger for the "serial discovery".
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('discovery-log'); // TODO: revert
})
.inSingletonScope()
.whenTargetNamed('discovery-log'); // TODO: revert
// Logger for the CLI config service. From the CLI config (FS path aware), we make a URI-aware app config.
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('config');
})
.inSingletonScope()
.whenTargetNamed('config');
// Logger for the monitor manager and its services
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(MonitorManagerName);
})
.inSingletonScope()
.whenTargetNamed(MonitorManagerName);
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(MonitorServiceName);
})
.inSingletonScope()
.whenTargetNamed(MonitorServiceName);
[
'daemon', // Logger for the Arduino daemon
'fwuploader', // Arduino Firmware uploader
'discovery-log', // Boards discovery
'config', // Logger for the CLI config reading and manipulation
'sketches-service', // For creating, loading, and cloning sketches
MonitorManagerName, // Logger for the monitor manager and its services
MonitorServiceName,
].forEach((name) => bindChildLogger(bind, name));
// Remote sketchbook bindings
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
@@ -423,3 +380,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(IsTempSketch).toSelf().inSingletonScope();
});
function bindChildLogger(bind: interfaces.Bind, name: string): void {
bind(ILogger)
.toDynamicValue(({ container }) =>
container.get<ILogger>(ILogger).child(name)
)
.inSingletonScope()
.whenTargetNamed(name);
}

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