Compare commits

..

221 Commits

Author SHA1 Message Date
Akos Kitta
750486a8f0 another approach
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-15 19:12:31 +02:00
Francesco Spissu
a04527d3b8 different prefix for temp example sketch 2022-09-15 19:12:31 +02:00
Francesco Spissu
33ec67109b mark as recently opened only sketches that not includes sketch_ in the name 2022-09-15 19:12:31 +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
Akos Kitta
4907ef2a47 Prepared 2.0.0-rc9.3.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 11:17:38 +02:00
github-actions[bot]
9ae3402631 Updated translation files 2022-09-05 10:22:13 +02:00
Akos Kitta
d0dfc656e6 Improved the scrolling UX in list widgets
- Fixed scrollbar does not reach end of list widget.
 - Estimated row heights to provide better scroll UX.
 - Last item's `<select>` must be visible.

Closes #1380
Closes #1381
Closes #1387

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 10:20:05 +02:00
Akos Kitta
df3a34eec6 Coerce a semver when calculating updatables.
Closes #1390

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 10:00:29 +02:00
Akos Kitta
20cc34ca9d Use CLI 0.27.0-rc.1.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-02 16:24:48 +02:00
Akos Kitta
1b7f86b231 Install the Arduino_BuiltIn to built-in location
Closes #1055.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-02 16:24:48 +02:00
Akos Kitta
0d545bea0e Show ports if has recognized board attached to it.
Closes #1365

 - Ref: 79ea0fa9a6
 - Ref: 74bfdc4c56

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-01 09:08:20 +02:00
Akos Kitta
204d71b2dd Fixed highlighting of non-unicode chars in Output
Closes #1210

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-31 21:16:07 +02:00
Akos Kitta
5cb9166c83 Implemented filter and update all for libs/boards.
Closes #177
Closes #1188

Co-authored-by: Francesco Spissu <f.spissu@arduino.cc>
Co-authored-by: Per Tillisch <p.tillisch@arduino.cc>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-31 10:07:27 +02:00
github-actions[bot]
7828cc11ac Updated translation files (#1305)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-31 09:50:01 +02:00
Akos Kitta
34a7fdb733 Pinned 63f1e18 CLI.
Ref: 63f1e1855a
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 18:35:11 +02:00
Akos Kitta
7c361cf2d1 Can close non-root sketch file editors.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
8beade0867 Fixed sketch content changes when renaming a file.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
3afc2d7e4b Fixed dirty indicator of uncloseable widgets.
Closes #1034.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
d40401437a removed space from discovery json log.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
10ac7fd50a Removed File > Close Editor.
Closes arduino/arduino-ide#660

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
07962e81d4 Moved uncloseable widget tracking to manager.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
785775327b Updated translations.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
80dfa5b7dd Restored logic to close current closable widget
and then the window.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
40425d49e0 Unified the sketch close and the app quit logic.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
0c87fa9877 Update currentSketch when files change.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
5b79320302 do not try to restore temp sketches.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
1da2dfc349 No save dialog prompt if closing untouched sketch.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
d7bbfc515d init
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Francesco Spissu
0c22884729 Error message if upload is not possible (#1353) 2022-08-26 11:24:03 +02:00
Akos Kitta
fc9107c084 Use addressLabel in the UI.
- for the boards dropdown, and
 - for the `Tools` > `Port` menu.

Closes #1331

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-25 11:49:50 +02:00
Akos Kitta
474d5e5975 Added a workaround for missing port#properties.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-25 11:49:50 +02:00
Akos Kitta
f7f644cf36 Use port properties from the discovery.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Closes #740
2022-08-25 11:49:50 +02:00
Dave Simpson
b5f9aa0f15 change hard coded max and min (#1345) 2022-08-24 12:32:42 +02:00
Dave Simpson
cc5cf3b165 fix 180: prevent erroneous "auto-reconnect"(s) in board selector (#1328)
* ensure auto-select doesn't select wrong port

prevent auto-select of unattached boards Co-authored-by: Francesco Spissu <francescospissu@users.noreply.github.com>

* clean up

* add "uploadInProgress" prop to boards change event

* remove unused methods and deps

* [WIP]: leverage upload event to derived new port

* remove timeout

* refine port selection logic

* clean up naming

* refine port selection logic & add delayed clean up

* renaming & refactoring

* method syntax & remove unnecessary methods
2022-08-24 12:31:51 +02:00
per1234
125bd64c91 Install Arduino CLI build dependencies in all dependent workflows
Arduino CLI is a tool dependency of Arduino IDE. For this reason, the necessary Arduino CLI build is acquired whenever
running the `yarn` command in the repository.

The way the Arduino CLI build is acquired depends on the type of version specified as dependency in the
`arduino.cli.version` field of the arduino-ide-extension package metadata:

- Release/nightly: download pre-built standard distribution
- Git ref: build from source

This means that, in the latter case, all build dependencies of Arduino CLI must be present. While the Go module
dependencies are automatically installed during the build, the build tool dependencies must be installed in advance:

- Go programming language
- Task task runner

Arduino IDE's infrastructure was recently changed to use the Task tool to build Arduino CLI in the supported manner. A
step to install Task was not added to some of the workflows that run `yarn`, which caused them to fail when a
non-release version of Arduino CLI was used as a dependency:

arduino-ide-extension: >>> Building the CLI...
arduino-ide-extension: /bin/sh: 1: task: not found
arduino-ide-extension: error Command failed with exit code 1.

A step for the missing tool dependency is hereby added to those workflows.

The lack of an explicit installation of the other dependency, Go did not result in an error because Go is pre-installed
on the GitHub Actions runner. However, the installed version may not match the version Arduino CLI is intended to be
built with and validated for, and the version provided by the runner may change at any time. For this reason, it will be
safest to explicitly set up the appropriate version of Go in the workflows.
2022-08-24 01:11:21 -07:00
per1234
ca47e8a09a Fix inconsistency of input field placeholder text capitalization
The board search input field of the "Select Other Board and Port" dialog uses placeholder text to explain the usage of
the field to the user.

All other placeholder text in the IDE's UI uses sentence case. This specific placeholder was the exception, using
unpleasant caps lock instead.

The inconsistency is resolved by changing the placeholder text to the standard sentence case.
2022-08-22 01:06:18 -07:00
per1234
52804a5b52 Add missing i18n for UI strings
The text of the Arduino IDE user interface has been localized to 12 languages.

Before localization can be accomplished, internationalization must be done in the application's code base:

- Set up infrastructure to export localization data
- Pass all target strings to that infrastructure

While the first of these tasks is completed, the second was not completed for several strings which are part of the user
interface.

Those outstanding strings are hereby internationalized and will be made available for localization.
2022-08-22 01:06:18 -07:00
per1234
3ec62642dd Fix typos in log messages
Several of the log messages contained minor typos.
2022-08-21 05:45:49 -07:00
per1234
1281ad1932 Use more relevant page for "Help > Environment" menu item target
Selecting "Help > Environment" from the Arduino IDE menus opens a page containing usage information for the Arduino IDE
application in the browser.

Previously, the URL used was the same as that of in Arduino IDE 1.x:

https://www.arduino.cc/en/Guide/Environment

The documentation from that page was written for Arduino IDE 1.x. Even though the UI of the two versions is aligned for
the most part, some advancements made for the 2.x major version series resulted in some differences. This means that
documentation targeted at Arduino IDE 1.x is not always applicable to Arduino IDE 2.x.

Fortunately, documentation is now available for each major version series of the IDE. So resolution is only a matter of
pointing the menu item at the correct URL.
2022-08-21 05:45:17 -07:00
Alberto Iannaccone
de32bddc20 Fix dialogs UI scalability (#1311)
* make dialogs scroll when scaling up the UI

* add unit of measure to settings step input

* wrap settings dialog items when scaling up the UI

* fix dialogs width when scaling up the UI

* rework board config UI to make it scale up better

* refactor ide updater dialog: move buttons outside the dialog content

* refactor ide updater dialog: clean-up code and rename events

* fix board config dialog title case and and remove double ellipsis
2022-08-18 16:42:16 +02:00
Akos Kitta
79ea0fa9a6 Show all network and serial ports.
Otherwise, unrecognized network boards are ignored
by IDE2.

Closes #1327

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 11:03:27 +02:00
Akos Kitta
683219dc1c Fixed typos.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 11:03:27 +02:00
Akos Kitta
d674ab9b73 Handle missing core when getting board user fields
Closes #1142

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 10:56:03 +02:00
Alberto Iannaccone
5be1f9d7fe change naming of nightly and snapshot builds (#1326)
replace `-` with `.` to make auto-update work correctly
2022-08-17 17:13:25 +02:00
per1234
9e2b73a045 Use unmodified official ClangFormat configuration as base formatter configuration (#1324)
The Arduino IDE's "Auto Format" feature is configured to produce the standard Arduino sketch formatting style by
default.

The Arduino IDE editor's default settings are compliant with that style. However, the user may adjust the editor
settings. In this case, the Arduino IDE automatically adjusts the Auto Format configuration to align with the user's
preferences.

The formatter configuration is consumed by several other projects in addition to the Arduino IDE. For this reason, the
configuration is hosted and maintained in a centralized location, from which it is pulled by all projects that use it.

Previously, the adjustment of the Arduino IDE formatter configuration according to the editor settings was integrated
into the configuration object itself. This meant that the standardized configuration had to be modified each time it was
pulled in to sync from the upstream source.

Moving the base formatter configuration object to a dedicated file, separated from the handling and adjustment code
allows syncs to be done by simply replacing the existing configuration file with the one automatically generated by the
CI system of the repository where the source configuration is hosted.
2022-08-16 08:09:39 -07:00
per1234
75e00c2bae Document clangd update procedure
Arduino IDE has dependencies on the clangd C++ language server and ClangFormat code formatter tools. These are updated
periodically to benefit from the ongoing development on those projects.

The update procedure requires operations in three different repositories:

- Generate builds in arduino/clang-static-binaries
- Validate and update formatter configuration in arduino/tooling-project-assets
- Update metadata in arduino/arduino-ide

Previously, this was undocumented and the procedure existed only in the form of "institutional memory".

The procedure is now fully documented in the readme of arduino-ide-extension.
2022-08-15 08:42:31 -07:00
Akos Kitta
989300f25d Close core error notification on subsequent action
Closes #1154

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 16:55:17 +02:00
Akos Kitta
5226636fed Link compiler errors to editor.
Closes #118

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 16:55:17 +02:00
Akos Kitta
8b3f3c69fc Use the refactored CLI in IDE2.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 09:19:57 +02:00
Akos Kitta
a39ab47e70 Use Task to build pinned CLI for IDE2.
Closes #1313

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-11 09:28:50 +02:00
Alberto Iannaccone
9cabd40429 2.0.0-rc9.2 (#1312)
* 2.0.0-rc9.2

* use arduino-cli version 0.26.0-rc1
2022-08-10 13:04:02 +02:00
Francesco Spissu
6e3681896c Add Auto Format item under the Edit menu (#1230) 2022-08-10 11:36:53 +02:00
Akos Kitta
8a1cabd2bc Defer notification area rendering until app ready.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-09 17:23:10 +02:00
Akos Kitta
7a3e6789d1 Defer settings/certificates load until app ready.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-09 17:23:10 +02:00
Akos Kitta
92bc5ecf7b Replaced the splash screen with a preload.
Added a bare minimum example.

Closes #193
Closes #324
Closes #327
Closes #717
Closes #851

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-09 17:23:10 +02:00
Francesco Spissu
aebec0f942 Live change of theme from Preferences dropdown (#1296) 2022-08-09 14:40:56 +02:00
per1234
54db9bbce8 Sync sketch formatter configuration from source
The Arduino IDE's "Auto Format" feature is configured to produce the standard Arduino sketch formatting style, as
established by the Arduino IDE 1.x formatter.

The configuration is consumed by several other projects which require the configuration in a YAML file. In order to
provide all the consumers with a single canonical source and to locate the infrastructure and activity related to the
maintenance of the file in a more appropriate repository, it is now hosted in a permanent location in the
`arduino/tooling-project-assets` repository.

The following changes have been made to the source configuration:

- Move documentation comments to a dedicated file in the upstream repository
- Make additional non-functional changes to the configuration format to facilitate maintenance
- Update to use the configuration API of ClangFormat 14.0.0

This last item did result in some functional changes to the configuration which will result in minor differences in the
formatter output.

These are actually reversions of unwanted differences from the Arduino IDE 1.x formatter output, which were unavoidable
when using the 11.0.1 version of ClangFormat in use at the time of the configuration's creation. These changes will
provide greater consistency during the migration from Arduino IDE 1.x to 2.x. The default output of the Arduino IDE
1.x formatter will continue to be considered the "gold standard" until Arduino IDE 2.x graduates from "pre-release"
status.

The Arduino IDE 2.x formatter configuration is fully customizable according to the preferences of each user. Those
already using custom configurations will not be affected in any way (though they are encouraged to sync their
configuration files from the source to bring them into compliance with the configuration API of the ClangFormat version
currently in use by Arduino IDE 2.x).

See the documentation and commit history for the source file for details on the configuration changes:

https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration
2022-08-08 12:48:41 -07:00
per1234
676eb2f588 Escape special characters in formatter configuration for Windows
The sketch code formatter configuration is passed to the ClangFormat tool as a string representing a JSON object via a
command line argument.

Previously, the contents of this string were not given any special treatment to ensure compatibility with the command
interpreter used on Windows machines. That did not result in problems only because the configuration didn't contain
problematic combinations of characters. This good fortune will not persist through updates to the configuration, so the
command must be properly processed.

The Windows command interpreter does not use the POSIX style backslash escaping. For this reason, escaped quotes in the
argument are recognized as normal quotes, meaning that the string alternates between quoted and unquoted states at
random. When a character with special significance to the Windows command interpreter happens to occur outside a quoted
section, an error results.

The solution is to use the Windows command interpreter's caret escaping on these characters. Since such an escaping
system is not recognized by POSIX shells, this is only done when the application is running on a Windows machine.

References:

- https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/echo#remarks
- https://en.wikipedia.org/wiki/Escape_character#Windows_Command_Prompt
2022-08-08 12:48:41 -07:00
per1234
ce273adf77 Correctly escape escaped content in formatter configuration
The sketch code formatter configuration is passed to the ClangFormat tool as a string representing a JSON object via a
command line argument.

The quotes in the JSON syntax are escaped in order to make them compatible with this usage. Previously, consideration
was not given to escaping of the content. For example, with the previous escaping code, this content: `\"` would be
converted to `\\"`, whereas the correct escaping would look like `\\\"`.

That did not result in problems only because the configuration didn't contain escaped content. This good fortune will
not persist through updates to the configuration so the command must be properly processed.

The content of the configuration will now be escaped in addition to the quotes of the JSON data format.
2022-08-08 12:48:41 -07:00
Akos Kitta
0b33b51700 Set XDG_CONFIG_HOME env on Linux when not set.
Otherwise, `node-log-rotate` creates a folder with `undefined` name.

Closes #394.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-04 16:29:27 +02:00
Akos Kitta
36ac47b975 Can check if the current window is the first one.
Closes #1070

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-04 11:11:46 +02:00
Akos Kitta
bf193b1cac Pinned 2dd8976 CLI in the IDE2. (#1280)
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-04 09:28:28 +02:00
InstantMuffin
879aedeaa3 Update BUILDING.md (#1281)
* Update BUILDING.md

Added "Notes for Linux contributors" based on my own building experience

* Update BUILDING.md

Removing the linux specific section and instead updating the Theia IDE prerequisites link to point to the mentioned file directly.
2022-08-03 16:43:01 +02:00
Akos Kitta
d556ee95c0 Use FQBN instead of Board for the monitor ID.
Closes #1278

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-03 15:16:39 +02:00
Alberto Iannaccone
d93c9ba654 2.0.0-rc9.1 (#1272) 2022-08-02 15:29:15 +02:00
Francesco Spissu
8a0dc1be7e Custom colors clean up (#1252) 2022-08-02 15:24:54 +02:00
Alberto Iannaccone
564862e173 Prevent board selector item labels to overflow (#1216)
* prevent board selector item labels to overflow

* make board selector show ellipsis when the board name is too long
2022-08-02 11:11:38 +02:00
Francesco Spissu
d7f7010bb5 High Contrast theme update (#1265) 2022-08-01 15:24:52 +02:00
Akos Kitta
e156dcc213 Show 'progress' indicator during verify/upload.
Closes #575
Closes #1175

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-01 15:07:14 +02:00
Akos Kitta
27a2a6ca03 #1191: resolve temp path if copying/cloning sketch
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-01 10:11:14 +02:00
Akos Kitta
581379f86f #1191: fixed default sketchbook URI for _save as_
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-01 10:11:14 +02:00
Akos Kitta
b62f3dec84 #714: UX improvements of the Arduino LS in IDE2
- Debounced the connectivity status update.
 - Silent the output channel for the Arduino LS.
 - Delay the problem markers update with 500ms.
 - Do not update the status bar on every `keypress` event.
 - Debounced the tab-bar toolbar updates when typing in editor.
 - Fixed electron menu contribution binding.
 - Aligned the editor widget factory's API to Theia.
 - Set the zoom level when the app is ready (Closes #1244)
 - Fixed event listener leak (Closes #1062)

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-01 10:11:14 +02:00
Akos Kitta
90d2950bdd Use 0.25.1 CLI.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-01 09:12:43 +02:00
github-actions[bot]
5b7d64c1c1 Updated translation files (#1269)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-01 08:22:58 +02:00
Dave Simpson
55927ac3dd remove state from stepper input and simplify (#1264)
* remove state from stepper input and simplify

* get rid of lodash
2022-07-29 17:44:58 +02:00
github-actions[bot]
40c93bc19a Updated translation files (#1249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-29 17:36:25 +02:00
Alberto Iannaccone
59b8a2d6bb Register custom themes after the monaco theme init (#1257)
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-29 15:09:53 +02:00
Alberto Iannaccone
124738d810 wait for language packs to be deployed (#1261) 2022-07-29 15:08:07 +02:00
Dave Simpson
19c0334a91 use fixed footer and overflow: auto for content (#1256) 2022-07-28 17:38:47 +02:00
Dave Simpson
f22be3c587 #1223: use theme service on settings load (#1238)
* use theme service on settings load

* use window.matchMedia in loadSettings

* typo fix

* Patched app config to dispatch on OS' theme.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-27 11:06:48 +02:00
Dave Simpson
9373a0bcaf #374: ensure compile verbose pref is included on upload (#1237)
* ensure compile verbose pref is included on upload

* better verbose typings

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-26 14:05:12 +02:00
Francesco Spissu
5087ff08f2 Primary action to the right of the notification box (#1234) 2022-07-20 16:49:30 +02:00
David Simpson
71d5a1520a use variable for step button container bkgnd (#1233) 2022-07-20 14:56:51 +02:00
Alberto Iannaccone
ec160df25e 2.0.0-rc9 (#1228) 2022-07-20 13:00:44 +02:00
github-actions[bot]
7fbf3dc656 Updated translation files (#1201)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-20 12:12:39 +02:00
Akos Kitta
7680194feb Use 0.25.0-rc2 CLI.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-19 17:09:17 +02:00
Francesco Spissu
2fdb19ea75 Resize sidebar icons (#1217) 2022-07-19 14:37:01 +02:00
Alberto Iannaccone
8610332afc Fix board selector synchronization (#1214)
* prevent deselecting a board from the board selctor

* orrectly update board selector when baord config changes
2022-07-19 14:25:23 +02:00
David Simpson
1f7c2eb52c Add typing support to steppers (#1209)
* add typing support to steppers

* logic cleanup

* misc cleanup

* account for lack of unmount
2022-07-19 13:07:39 +02:00
Francesco Spissu
119dfa78d9 Restore the debug button in toolbar (#1215) 2022-07-19 13:00:25 +02:00
Akos Kitta
337d22efbd Dropped compile.optimizeForDebug preference.
Closes #1212.

Restored the `Optimize for Debugging` before:
abca14a02be77160a86d9f4fb6eca8c18d47312d2d4be37c50de50430bbbcd07

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-19 12:46:39 +02:00
Francesco Spissu
5ff9ce0028 Toolbar enhancements (#1194) 2022-07-18 18:43:41 +02:00
Akos Kitta
d4833affc6 #1207: Forward the realTimeDiagnostics to the LS.
Closes #1207.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-18 17:21:00 +02:00
Akos Kitta
8ad10b5adf #1089: IDE2 falls back to new sketch if opening failed. (#1152)
IDE2 falls back to a new sketch if the opening fails.

Closes #1089

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-18 11:10:33 +02:00
Alberto Iannaccone
fe31d15b9f Localize commands (#1196)
- "check for updates"
- "open serial plotter"
2022-07-18 10:47:44 +02:00
Alberto Iannaccone
99664ee544 avoid using useContentSize when creating a new window (#1197) 2022-07-18 10:46:30 +02:00
Akos Kitta
57841b3c0a #714: Use the build cache to speed up the LS (#1107)
* Notify the LS about the new `build_path` after verify.

Closes #714

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-18 10:19:00 +02:00
Francesco Spissu
ed41b25889 IDE startup theme based on OS theme (#1160)
* add patch for setting IDE startup theme based on OS theme

* Patched the default theme behavior.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* add custom themes in register

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-18 09:03:43 +02:00
Alberto Iannaccone
4f27725b35 New Board Selector UI: show port protocol (#1193)
* add new icons

* implement new Board Selector design

* make board selector item focusable

* fix i18n

* 💄

* re-add debug log on board config changed

* Updated themes

* use new color variables

* update arduino-icons.json

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-15 18:04:18 +02:00
Alberto Iannaccone
73835eced3 Prevent overwriting existing libraries and platforms at first IDE start-up (#1169)
* move initialization of libs and platforms into new contribution

* use noOverwrite when install built-in libraries and platform

* catch errors when installing platforms and libraries at first start-up

* arduino-cli version 0.25.0-rc1

* refine platforms and libraries initialization in case of errors

* add trailing newline when libraries and platform installation fail

* use regex to check error if builtin library dependencies are already installed

* rename contribution
2022-07-15 16:06:15 +02:00
Alberto Iannaccone
46fcc71dd8 add language packs (#1166) 2022-07-15 14:10:35 +02:00
Francesco Spissu
453a657172 sketchbook item selected bg update (#1190) 2022-07-15 14:09:36 +02:00
github-actions[bot]
1514d014a9 Updated themes (#1187)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-15 13:04:22 +02:00
github-actions[bot]
e4d9243486 Updated translation files (#1164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-14 23:05:24 +02:00
Alberto Iannaccone
fb690c97e8 Fix settings dialog size (#1172)
* give an id to the settings dialog to grant higher priority to css rule to fix the max-width

* fix settings dialog height
2022-07-14 14:50:46 +02:00
Akos Kitta
a0038315da fixup.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-14 10:39:54 +02:00
Akos Kitta
aea550fe33 rename
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-14 10:39:54 +02:00
Akos Kitta
813444408e removed unused logger
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-14 10:39:54 +02:00
Akos Kitta
d8be8888ef another way to cancel the discovery.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-14 10:39:54 +02:00
Akos Kitta
431c3bdf2b Restart discovery after re-initializing client.
Otherwise, board discovery stops working after indexes update.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-14 10:39:54 +02:00
Francesco Spissu
c51b201362 Avoid twice serial plotter apps (#1174)
* avoid twice serial plotter apps

* remove copy-serial-plotter script.

* Use `require#resolve` to locate the plotter app. (#1178)

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

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

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-13 17:20:11 +02:00
Akos Kitta
7fed8febf1 Let DI framework create MonitorService instances
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-12 15:59:33 +02:00
Akos Kitta
f4a68e793e Fixed missing core client in the monitor service.
Restored monitor service creation state before a36524e:
Pass core client provider into new instances as a field.

Closes #1161

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-11 08:27:43 +02:00
Francesco Spissu
7d961537eb Increase space between input and controls in dialogs (#1159) 2022-07-08 16:10:02 +02:00
Francesco Spissu
d7a2d83990 Update buttons style (#1122)
* Buttons updated to reflect the design system.
2022-07-08 10:43:10 +02:00
Akos Kitta
a36524e02a Update package index on 3rd party URLs change.
Closes #637
Closes #906

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-08 09:04:10 +02:00
github-actions[bot]
1073c3fc7d Updated translation files (#1052)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-07 17:40:37 +02:00
Alberto Iannaccone
69d7e8e96c Window min size (#1151)
* set min widow size

* format document

* fix dialogs sizes
2022-07-07 16:14:46 +02:00
David Simpson
7f2b849963 #854 fix platform installation only offered if port is selected (#1130)
* ensure desired prompts shown + refactor

* pr review changes
2022-07-06 08:38:51 +02:00
Alberto Iannaccone
0ce065e496 disable survey contribution (#1150) 2022-07-05 17:44:17 +02:00
David Simpson
0b0958c20e change output buffer to setTimeout instead of setInterval (#1123)
* change output buffer to setTimeout

* remove unnec. code

* dispose buffer on end, not 'finally'

* revert core-service changes

* refactor, disposable pattern

* newline
2022-07-05 16:27:37 +02:00
Francesco Spissu
06acd7fcde Set sketchbook list item height to 30px (#1146) 2022-07-05 14:21:40 +02:00
Francesco Spissu
b1e00e6ff2 Increase sketchbook tree indentation to reflect design system (#1148) 2022-07-05 14:10:56 +02:00
Akos Kitta
ea42dc52fd Sketchbook handles more than two tree levels.
Use a default `false` value for the `explorer.compactFolders` preference

Closes #1015.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-05 09:40:35 +02:00
github-actions[bot]
6586cb37a8 Updated themes (#1145)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-04 18:37:22 +02:00
github-actions[bot]
9b7ab14253 Updated themes (#1141)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-04 16:31:22 +02:00
Alberto Iannaccone
d6899af5e7 fix cloud sketchbook widget rendering empty (#1101) 2022-07-04 15:52:57 +02:00
Alberto Iannaccone
087cab177b Sketchbook sidebar state (#1102)
* add commands to open sketchbook widgets

add commands to show sketchbook widgets

* enable sending commands via query params

* opening sketch in new window will open sketchbook

* requested changes

* add specific method WorkspaceService to open sketch with commands

* add encoded commands contribution

* try merge show sketchbook commands

* pair session changes.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* i18n fixup.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* minimized scope of hacky code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* clean up OPEN_NEW_WINDOW command

* add comment on workspace-service.ts

* reveal node with URI

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-04 15:49:25 +02:00
David Simpson
5da558dfd9 remove border from tree indent (#1140)
* remove border from tree indent

* use config change instead of css override
2022-07-04 15:00:06 +02:00
David Simpson
953859831c invert uninstalled pkg comparator + cleanup (#1131) 2022-07-04 09:11:58 +02:00
Francesco Spissu
a13a8771d1 Remove tabs context menu (#1128) 2022-07-01 09:18:30 +02:00
Francesco Spissu
5499c25528 Sketchbook icons colors (#1095) 2022-06-29 16:51:48 +02:00
per1234
1e469627b4 Only run "Arduino IDE" workflow on relevant changes
The "Arduino IDE" workflow performs the following operations when triggered on push and pull request events:

- Build application
- Lint code
- Run tests
- Produce tester packages

All of these operations are specific to the TypeScript/JavaScript code base and its infrastructure.

Previously, the workflow ran whenever any file in the repository was changed. This includes files that have no
relevance, meaning the operations performed by the workflow were pointless. In addition to general inefficiency, these
lengthy and sometimes spuriously failing unnecessary workflow runs might cause delay or confusion to both the
contributors and maintainers for what would otherwise be a simple process.

GitHub Actions provides the ability to configure path filters for the workflow triggers. The workflow will only run on
events that change files satisfying these path filters. This is "AND"ed with the `branches` filters, meaning the existing
restrictions on which branches produce a run remain unchanged. The `tags` filter is independent from the `paths` and
`branches` filters, meaning the added path filters don't make any change to which tag push events will trigger the
workflow.
2022-06-29 03:51:06 -07:00
per1234
34ef25c4e4 Enable "Arduino IDE" workflow use by contributors
GitHub Actions workflows may require access to privileged information in order to perform certain operations. GitHub
provides the capability for doing this via "repository secrets".

For security reasons, repository secrets are only accessible to a GitHub Actions workflow run when it is triggered by an
event from within the repository containing the secret. This means that a workflow which requires such secrets would
fail when run in a fork (unless the fork owner was able to set up their own secrets with suitable values).

In order to make the relevant components of the CI system friendly for use in forks by contributors validating their
work in preparation for submitting a PR, when the operations that require access to a secret are supplemental, those
operations should be configured to only run from branches of the parent repository.

Due to its unfortunate monolithic design, in addition to operations useful to contributors, the "Arduino IDE" workflow
contains several such supplemental operations:

- Code signing
- Publishing release artifacts to Arduino's server

Some attempt was previously made to configure the workflow to skip these operations when run in forks, but that
configuration was not done correctly. This made the workflow only usable by contributors with a deep enough
understanding of GitHub Actions to be able to make the necessary modifications provisionally every time they needed to
use the workflow.

The average contributor would not be capable or willing to do this, which might result in PRs being
submitted in a less validated state, increasing the burden on maintainers.

The specific misconfigurations:

**`build` job was conditional on the workflow running from `arduino/arduino-ide`**

The job itself can run just fine in a fork, so there is no reason to impose this restriction.

Since the time this conditional was added, some changes have been made to the GitHub Actions system which makes this
sort of configuration unnecessary:

- GitHub Actions is globally disabled in forks by default
- Workflows which contain a `schedule` trigger (as is the case with this one) are individually disabled by default,
  requiring the repository owner to enable it specifically even after enabling GitHub Actions in general.

This means this workflow will never run unexpectedly in a fork. The fork owner will always have intentionally enabled it.

So this conditional can be removed completely.

**Code signing was conditional on PR being submitted from a branch of the base repo**

This would cause a spurious failure of the signing operation on PRs made within the contributor's fork when the signing
secrets were not defined.

The more appropriate condition of whether the signing secrets are defined or not is now used. The environment variable
name has been updated accordingly.

**`release` job was conditional on running from `arduino/arduino-ide`**

The GitHub release creation step of this job can run in any repository. It is only the step that uploads to Arduino's
AWS server which would only make sense to run from `arduino/arduino-ide`.

So the conditional is moved to the AWS upload step, allowing contributors to test the workflow's release operation in
their forks to validate related proposals.
2022-06-28 10:36:03 -07:00
per1234
d1aa446c89 Refactor signing certificate handling in "Arduino IDE" workflow
Previously, there was some code duplication of the complex code signing certificate handling commands, which made the
related code more difficult to understand, maintain, and develop.

The cause of this duplication is that there is a separate certificate for each operating system, each of which is stored
in separate repository secrets, as well as a different certificate file extension for each OS. Since the secret names
and file extensions are associated with the operating system, it is most logical to define them via attributes alongside
the operating system definition in the job matrix configuration already used to generate the parallel job runs for
native build on each OS.

That done, the certificate handling commands are universal and the system can easily expand to additional host targets
(e.g., Apple M1) as time goes on.
2022-06-28 10:36:03 -07:00
per1234
e454acba41 Remove obsolete compilation error interpretations
The Arduino IDE attempts to provide some additional guidance to users based on matches against compilation error
messages.

This practice was established during a time when some significant breaking changes were made to the common APIs in order
to ease the transition.

Since that time, the practice has mostly been discontinued. The interpretations are only valid for very old code that is
unlikely to be used by the target users now. So their benefit is negligible. The patterns used are inexact, meaning that
the interpretations may be printed inappropriately, which is more and more likely as the cases where the matches would
be valid become increasingly rare. When the maintenance burden is taken into consideration, it is clear that the harm is
far more than any benefits from these. So they are removed.

Notes for specific interpretations:

> Please import the SPI library from the Sketch > Import Library menu.
> As of Arduino 0019, the Ethernet library depends on the SPI library.
> You appear to be using it or another library that depends on the SPI library.

The target error was more common prior to Arduino IDE 1.6.6 (released ~6.5 years ago), when it was necessary for the
sketch to contain `#include` directives for transitive in addition to direct library dependencies (SPI is a common
transitive dependency).

Due to the nature of the SPI library, it is not often used directly, and when it is used directly it is done by more
advanced users who are unlikely to forget the `#include` directive and would have no need for this interpretation even
if they did.

It is far more likely for the user to forget an `#include` for a popular library, yet Arduino rightly does not attempt
to maintain interpretations for those.

The "Sketch > Import Library" menu path was renamed to "Sketch > Include Library" ~7 years ago.

Arduino IDE 0019 was released ~12 years ago. We can safely assume the migration to the new Ethernet API is complete.

> The 'BYTE' keyword is no longer supported.
> As of Arduino 1.0, the 'BYTE' keyword is no longer supported.
> Please use Serial.write() instead.

Arduino IDE 1.0 was released ~10.5 years ago. We can safely assume the migration to the new Serial API is complete.

This compilation error pattern is now far more likely to occur due to incorrect usage of a completely unrelated
occurrence of the common `BYTE` name in the user's code.

> The Server class has been renamed EthernetServer.
> As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.

> The Client class has been renamed EthernetClient.
> As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.

> The Udp class has been renamed EthernetUdp.
> As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.

Arduino IDE 1.0 was released ~10.5 years ago. We can safely assume the migration to the new Ethernet API is complete.

The compilation error patterns are in no way specific to the Ethernet library so is prone to false positives.

> Wire.send() has been renamed Wire.write().
> As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.

> Wire.receive() has been renamed Wire.read().
> As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.

Arduino IDE 1.0 was released ~10.5 years ago. We can safely assume the migration to the new Wire API is complete.

Due to the nature of the Wire library, it is not often used directly, and when it is used directly it is done by more
advanced users who have less need for an interpretation of the compiler error.

> 'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?

> 'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?

I left these in because they are the most "recent" (added due to a breaking change made 7 years ago).

However, I also feel that these are harmful and should either be removed or changed. The problem is that there is a
false match when the user attempts to compile the Keyboard or Mouse libraries for a board which does not have native USB
support (e.g., Uno, Mega), even when their sketch does contain the `#include` directives that are recommended by the
interpretation. That cause of the compilation error matching the pattern is more common than the case where the user is
compiling old code or forgot the `#include` directive, for which the interpretation is valid.
2022-06-28 08:21:54 -07:00
github-actions[bot]
75abb70bcd Updated themes (#1125)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-28 16:38:04 +02:00
per1234
7ba98a212c Run "Arduino IDE" workflow on pull requests with any base branch
Contributors may submit pull requests against development branches in the repository for either of the following valid
reasons:

- Propose changes to a previous proposal, either while it is still in development, or else in the case where the changes
  are more complex/extensive than can be efficiently proposed via the PR review framework.
- The proposal is dependent on work from an unmerged PR.

Previously, the "Arduino IDE" GitHub Actions workflow was unnecessarily configured to only run for PRs based on the
`main` branch. This meant that validation and tester builds were not provided for the PRs based on other branches.
2022-06-28 00:35:45 -07:00
Francesco Spissu
6ae6ba5b3d Add missing Advanced string (#1104) 2022-06-27 10:08:31 +02:00
Alberto Iannaccone
439cdfbbff 2.0.0-rc8 (#1105) 2022-06-23 11:33:27 +02:00
Alberto Iannaccone
672fd4e4b0 bump arduino-cli version to 0.24.0 (#1103) 2022-06-23 10:59:07 +02:00
David Simpson
0f1d379e58 reference cli rc1 0.24 in package.json (#1098) 2022-06-22 18:54:30 +02:00
Francesco Spissu
a79c9b4449 Sketchbook tree indentation (#1097) 2022-06-22 18:23:14 +02:00
Akos Kitta
0f8a29a493 Disabled MenuItem roles on macOS.
Closes #969
Upstream-ref: eclipse-theia/theia#11217

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 12:17:36 +02:00
David Simpson
a54d7c8f45 #1032 failing upload flag for monitor mgr (#1040)
* 1032 failing upload flag for monitor mgr

* move upload failure fix logic to frontend

* misc corrections

* avoid starting monitor when upload is in progress

* avoid starting monitor when upload is in progress

* prevent monitor side effects on upload (WIP)

* send upload req after notifying mgr

* dispose instead of pause on upld (code not final)

* Revert "dispose instead of pause on upld (code not final)"

This reverts commit 2d5dff2a2d.

* force wait before upload (test)

* always start queued services after uplaod finishes

* test cli with monitor close delay

* clean up unnecessary await(s)

* remove unused dependency

* revert CLI to 0.23

* use master cli for testing, await in upload finish

* remove upload port from pending monitor requests

* fix startQueuedServices

* refinements queued monitors

* clean up monitor mgr state

* fix typo from prev cleanup

* avoid dupl queued monitor services

* variable name changes

* reference latest cli commit in package.json

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2022-06-22 10:39:14 +02:00
Akos Kitta
84109e416a Fixed widget lookup to eliminate duplicate tabs.
- Removed `@theia/editor-preview`,
 - Patched opener options when repairing layout on start, and
 - Compare widget keys with deepEquals instead of string equal.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 10:11:11 +02:00
Francesco Spissu
083337de1c IDE updater dialog colors fix (#1092) 2022-06-21 18:04:28 +02:00
Francesco Spissu
bd6bc135fd Remote sketchbook tooltips (#1088)
* rename Cloud Sketchbook in Remote Sketchbook

* add tooltips for Sync and Account buttons
2022-06-21 18:03:39 +02:00
Akos Kitta
4611381a38 Merged in #1074.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
Akos Kitta
d6f4096cd0 Reveal the error location after on failed verify.
Closes #608
Closes #229

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
David Simpson
a715da3d18 flush on clear output buffer (#1074) 2022-06-20 09:32:10 +02:00
Akos Kitta
94ceefd960 Can enable debug logging of the gRPC calls.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 14:14:47 +02:00
Akos Kitta
27dd120e5d Cleaned up File menu.
Removed:
 - `New File`,
 - `New Window`.

Closes #1014.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 13:11:08 +02:00
Francesco Spissu
f5cee97fef Implement survey notification (#1035) 2022-06-17 10:17:42 +02:00
Akos Kitta
a9aac0dbb0 Bound the original handler to this.
Closes #977

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-14 14:58:48 +02:00
David Simpson
4c6243176c Output panel optimisation (#1058)
* test interval for output panel

* create buffer provider

* output panel buffer corrections

* output buffer cleanup

* code cleanup
2022-06-14 13:00:20 +02:00
Akos Kitta
a8047660a6 Restored the Settings UI. Deferred model loading.
Closes #1031

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:08:35 +02:00
Akos Kitta
7c2843f7fd Relaxed the error handling of the core client init
For example, `malformed custom board options` was incorrectly detected
as loading JSON index file error.

Closes #1036

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:06:58 +02:00
Alberto Iannaccone
fd5154ae93 2.0.0-rc7 (#1027) 2022-06-09 10:14:56 +02:00
Alberto Iannaccone
726628e20c Fix monitor service id creation (#1025) 2022-06-08 17:21:26 +02:00
Akos Kitta
585a82b51a Added logging when restoring the layout data.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Akos Kitta
5edccb9c35 Avoid opening duplicate editor tabs.
Customized the shell layout restorer:
 - If a resource is about to open in code editor and preview,
do not open the preview.
 - If a resource is about to open in preview only, open a code
editor instead.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Akos Kitta
555da878f4 Editor manager should be singleton.
Added some logging when filtering the layout data.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Alberto Iannaccone
df8658eff9 Pluggable monitor (#982)
* backend structure WIP

* Scaffold interfaces and classes for pluggable monitors

* Implement MonitorService to handle pluggable monitor lifetime

* Rename WebSocketService to WebSocketProvider and uninjected it

* Moved some interfaces

* Changed upload settings

* Enhance MonitorManager APIs

* Fixed WebSocketChange event signature

* Add monitor proxy functions for the frontend

* Moved settings to MonitorService

* Remove several unnecessary serial monitor classes

* Changed how connection is handled on upload

* Proxied more monitor methods to frontend

* WebSocketProvider is not injectable anymore

* Add generic monitor settings storaging

* More serial classes removal

* Remove unused file

* Changed plotter contribution to use new manager proxy

* Changed MonitorWidget and children to use new monitor proxy

* Updated MonitorWidget to use new monitor proxy

* Fix backend logger bindings

* Delete unnecessary Symbol

* coreClientProvider is now set when constructing MonitorService

* Add missing binding

* Fix `MonitorManagerProxy` DI issue

* fix monitor connection

* delete duplex when connection is closed

* update arduino-cli to 0.22.0

* fix upload when monitor is open

* add MonitorSettingsProvider interface

* monitor settings provider stub

* updated pseudo code

* refactor monitor settings interfaces

* monitor service provider singleton

* add unit tests

* change MonitorService providers to injectable deps

* fix monitor settings client communication

* refactor monitor commands protocol

* use monitor settings provider properly

* add settings to monitor model

* add settings to monitor model

* reset serial monitor when port changes

* fix serial plotter opening

* refine monitor connection settings

* fix hanging web socket connections

* add serial plotter reset command

* send port to web socket clients

* monitor service wait for success serial port open

* fix reset loop

* update serial plotter version

* update arduino-cli version to 0.23.0-rc1 and regenerate grpc protocol

* remove useless plotter protocol file

* localize web socket errors

* clean-up code

* update translation file

* Fix duplicated editor tabs (#1012)

* Save dialog for closing temporary sketch and unsaved files (#893)

* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

* Fixed FS path vs encoded URL comparision when handling stop request.

Ref: https://github.com/eclipse-theia/theia/issues/11226
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations again.

Removed `electron` from the `nls-extract`. It does not contain app code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Aligned the stop handler code to Theia.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

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

* fix serial monitor send line ending

* refactor monitor-service poll for test/readability

* localize web socket errors

* update translation file

* Fix duplicated editor tabs (#1012)

* i18n:check rerun

* Speed up IDE startup time.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* override coreClientProvider in monitor-service

* cleanup merged code

Co-authored-by: Francesco Stasi <f.stasi@me.com>
Co-authored-by: Silvano Cerza <silvanocerza@gmail.com>
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 15:51:12 +02:00
Akos Kitta
4c55807392 Speed up IDE startup time.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 13:46:29 +02:00
github-actions[bot]
cb50d3a70d Updated translation files (#974)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-07 12:19:36 +02:00
David Simpson
eaf14aa1eb Follow up 944: authentication sessions are not persistent (#1003)
* #944: Fixed auth. sessions not persistent

* 944: Prevent race conditions setting authOptions

* typo correction, duplicate identifier

* prevent block of auth client service on setOptions

* consider windows cred. mgr. password len limit
2022-06-07 11:46:28 +02:00
Akos Kitta
a59e0da2af Use clang-format as the default sketch formatter.
- Bumped `clangd` to `14.0.0`,
 - Can use `.clang-format` from:
   - current sketch folder,
   - `~/.arduinoIDE/.clang-format`,
   - `directories#data/.clang-format`, or
   - falls back to default formatter styles.

Closes #1009
Closes #566

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 10:51:45 +02:00
Francesco Spissu
3a3ac6da4e Dark theme implementation (#991) 2022-06-07 10:48:45 +02:00
Akos Kitta
d7809616a4 Fixed LS stops working after OS sleep/wakeup cycle
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 10:31:05 +02:00
Mark Sujew
5b486b1480 Save dialog for closing temporary sketch and unsaved files (#893)
* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

* Fixed FS path vs encoded URL comparision when handling stop request.

Ref: https://github.com/eclipse-theia/theia/issues/11226
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations again.

Removed `electron` from the `nls-extract`. It does not contain app code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Aligned the stop handler code to Theia.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-01 10:55:08 +02:00
Mark Sujew
5fc30bd33e Fix duplicated editor tabs (#1012) 2022-05-31 11:33:07 +02:00
Akos Kitta
522a5c6e01 Relaxed the Node version: ^14.x
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-25 10:05:20 +02:00
Mark Sujew
1ae60ec9bc Updated Theia to 1.25.0
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-25 10:05:20 +02:00
David Simpson
b8c718ce9e #944: Fixed auth. sessions not persistent (#992) 2022-05-23 09:52:44 +02:00
Akos Kitta
b407d0aee0 #985: Restored the missing inject decorator.
Closes #985.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-13 09:20:00 +02:00
per1234
289f9d7946 Allow flexibility in OS type selections in issue forms
GitHub issue forms are used in this repository to facilitate the creation of high quality issues. These provide input
fields for each of the distinct classes of information which will be essential for the evaluation of the issues.

One of these fields is for the user's operating system. A dropdown menu is used for the selection of the high level
operating system type. Previously this only permitted the selection of a single option. A devoted contributor might have
made the effort to determine that the issue applies to multiple operating system types only to be met with the inability
to provide this information via the dedicated field.

The field also did not offer an option to indicate that the operating system was irrelevant to the issue (e.g., a
subject related to the repository assets).

Those issues are resolved by the following changes:

- Configure the field to allow multiple selections
- Add a "N/A" option to the menu
2022-05-05 02:27:22 -07:00
github-actions[bot]
905b78008d Updated translation files (#968)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-29 15:44:04 +02:00
Mark Sujew
11961bb7c7 Save all open editors before running Save As (#939)
* Save all open editors before running `Save As`

* Only save unsaved changes to new sketch
2022-04-29 15:42:48 +02:00
Alberto Iannaccone
2be1fac585 ignore workspace root check in changelog workflow (#960) 2022-04-20 15:23:52 +02:00
Alberto Iannaccone
b35340caa9 2.0.0-rc6 (#955) 2022-04-20 11:53:06 +02:00
Alberto Iannaccone
e6b3e2ec23 fix update version script (#958) 2022-04-19 16:04:08 +02:00
Mark Sujew
c07232698c Allow to close files in certain folders (#946)
* Allow to close files in certain folders

* Only direct children are sketch files
2022-04-19 12:00:15 +02:00
github-actions[bot]
58e992af13 Updated translation files (#959)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-19 11:59:58 +02:00
Alberto Iannaccone
a44b84ffd0 set the current language on the localization provider (#957) 2022-04-15 15:54:37 +02:00
Alberto Iannaccone
a3640cf812 use electron reload command when changing language (#953) 2022-04-14 09:38:23 +02:00
github-actions[bot]
03a75273e3 Updated translation files (#950)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-12 11:36:08 +02:00
Alberto Iannaccone
6176e50acf Enable language packs (#941)
* install language packs

* register localization contribution to backend module

* copy i18n folder to build

* fix chinese language iid
2022-04-08 14:59:11 +02:00
Alberto Iannaccone
46a3466bc5 improve check of read-only files (#918) 2022-04-07 16:45:09 +02:00
Alberto Iannaccone
aba9db6a6b Correctly print backslash-escaped characters (#943) 2022-04-06 18:05:32 +02:00
per1234
e5b34624ac Disable automatic application start after install via Windows Installer (#942)
Arduino IDE is packaged for Windows in multiple formats:

- ZIP
- NSIS
- Windows Installer (AKA "MSI")

The interactive installer of the NSIS package makes it the best option for installation by users.

The other use case for the installers is deployment by a system administrator. The Windows Installer package was added
to offer an additional installer option for this specific use case.

In this use case, a "silent install" will often be required. Previously, the Windows Installer package was configured to
start the Arduino IDE after completing the installation. This behavior is likely to be problematic for the very use case
the Windows Installer package was intended for. That configuration was not intentional, but rather a result of using
whatever setting electron-builder happened to provide as a default.

The behavior of the Windows Installer package is hereby changed to not run after installation. This also aligns it with
the behavior of the NSIS package's silent installation (running the installer with the `/S` flag).

The behavior of the NSIS installer is unchanged:

- When in interactive mode: user chooses whether to start Arduino IDE
- When in silent mode: Arduino IDE does not start after installation
2022-04-06 10:56:33 +02:00
Mark Sujew
c430cf0d88 Disable widget dragging/splitting (#940) 2022-04-05 12:21:49 +02:00
github-actions[bot]
1969e292f0 Updated translation files (#768)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-04 16:58:04 +02:00
Akos Kitta
0db119d7ba #919, #881: Fixed 3rd party URLs-related issues (#920)
* Fixed empty string to URLs conversion

Closes #919.

Signed-off-by: Akos Kitta <kittaakos@gmail.com>

* #881: Fixed height of the 3rd part URLs `textarea`

Closes #881.

Signed-off-by: Akos Kitta <kittaakos@gmail.com>
2022-04-04 16:52:55 +02:00
Francesco Spissu
c9b498fb08 add notes for Windows contributor in BUILDING.md (#926)
* add notes for Windows contributor in BUILDING.md

* rephrase notes for Windows contributor in BUILDING.md

* Update notes for Windows contributor in BUILDING.md

Co-authored-by: per1234 <accounts@perglass.com>

* move Notes for Windows contributors in Build from source section

Co-authored-by: per1234 <accounts@perglass.com>
2022-03-29 17:53:16 +02:00
Akos Kitta
78004fa4ca Minified browser code in the packaged final app. (#931)
- Also switched to minified `monaco` code,
- Removed dead code from the packaged.

Signed-off-by: Akos Kitta <kittaakos@gmail.com>
2022-03-29 17:45:54 +02:00
Mark Sujew
4de7737d14 Automatically remove editors for deleted files (#894) 2022-03-21 10:44:51 +01:00
per1234
f36df02f5d Switch to form-based GitHub issue templates
This project provides the contributors with templates for the fundamental categories of issues:

- bug report
- feature request

This is helpful to the maintainers and developers because it establishes a standardized framework for the issues and
encourages the contributors to provide the essential information.

GitHub's original issue template system is very crude, simply pre-populating the issue description field with the text
from the template file.

https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#creating-issue-templates

The contributor may be confused by being presented with a mass of Markdown and placeholder
content where they expected a field to write their issue. They also may find it inconvenient to manuever around the
framework content and replace the placeholder content.

A far better system is now available with GitHub's recently introduced form-based issue templates:

https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#creating-issue-forms

The user is now presented with a web form. These may include multi-line input fields that have the same formatting and
attachment capabilities as the standard GitHub Issue composer, but also additional elements such as menus and checkboxes.

The use of this form-based system should provide a much better experience for the contributors and also result in higher
quality issues.
2022-03-17 03:20:12 -07:00
per1234
753872ea2a Add links for other communication channels to the GitHub issue template chooser
The automatically created issue template chooser provides a menu of links to the available issue report templates as
well as the security policy at the start of the issue creation process.

It is also possible to add additional arbitrary items to the chooser, through GitHub's "Contact Links" feature. These
are defined in a configuration file:

https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser

These links offer the possibility to redirect support requests and other sub-optimal usages of the issues to the
appropriate location. This benefits the user by providing them with a fast and frictionless path to what they need, and
benefits the maintainers by preventing inappropriate issues.
2022-03-17 03:18:41 -07:00
Mark Sujew
ca1c24050d Fix Find Next command 2022-03-15 14:48:32 +01:00
Alberto Iannaccone
61c2b1a007 Install oktokit in changelog workflow (#901)
* install oktokit in changelog workflow

* fix how the old changelog is cut off
2022-03-14 12:05:53 +01:00
451 changed files with 38746 additions and 18497 deletions

View File

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

74
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Bug report
description: Report a problem with the code or documentation in this repository.
labels:
- "type: imperfection"
body:
- type: textarea
id: description
attributes:
label: Describe the problem
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To reproduce
description: Provide the specific set of steps we can follow to reproduce the problem.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What would you expect to happen after following those instructions?
validations:
required: true
- type: input
id: project-version
attributes:
label: Arduino IDE version
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
description: Which operating system(s) are you using on your computer?
multiple: true
options:
- Windows
- Linux
- macOS
- N/A
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating system version
description: Which version of the operating system are you using on your computer?
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any additional information here.
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Issue checklist
description: Please double-check that you have done each of the following things before submitting the issue.
options:
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
required: true
- label: My report contains all necessary details
required: true

View File

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

19
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
# Source:
# https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/template-choosers/general/config.yml
blank_issues_enabled: false
contact_links:
- name: Learn about using this project
url: https://github.com/arduino/arduino-ide#readme
about: Detailed usage documentation is available here.
- 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/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

View File

@@ -0,0 +1,69 @@
name: Feature request
description: Suggest an enhancement to this project.
labels:
- "type: enhancement"
body:
- type: textarea
id: description
attributes:
label: Describe the request
validations:
required: true
- type: textarea
id: current
attributes:
label: Describe the current behavior
description: |
What is the current behavior of the Arduino IDE in relation to your request?
How can we reproduce that behavior?
validations:
required: true
- type: input
id: project-version
attributes:
label: Arduino IDE version
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
description: Which operating system(s) are you using on your computer?
multiple: true
options:
- Windows
- Linux
- macOS
- N/A
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating system version
description: Which version of the operating system are you using on your computer?
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any additional information here.
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Issue checklist
description: Please double-check that you have done each of the following things before submitting the issue.
options:
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
required: true
- label: My request contains all necessary details
required: true

View File

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

View File

@@ -4,28 +4,52 @@ on:
push:
branches:
- main
paths-ignore:
- '.github/**'
- '!.github/workflows/build.yml'
- '.vscode/**'
- 'docs/**'
- 'scripts/**'
- 'static/**'
- '*.md'
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
pull_request:
branches:
- main
paths-ignore:
- '.github/**'
- '!.github/workflows/build.yml'
- '.vscode/**'
- 'docs/**'
- 'scripts/**'
- 'static/**'
- '*.md'
schedule:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
JOB_TRANSFER_ARTIFACT: build-artifacts
CHANGELOG_ARTIFACTS: changelog
jobs:
build:
if: github.repository == 'arduino/arduino-ide'
name: build (${{ matrix.config.os }})
strategy:
matrix:
config:
- os: windows-2019
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
certificate-extension: pfx # File extension for the certificate.
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
- os: macos-latest
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12
runs-on: ${{ matrix.config.os }}
timeout-minutes: 90
@@ -44,6 +68,17 @@ jobs:
with:
python-version: '3.x'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Package
shell: bash
env:
@@ -55,29 +90,21 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
run: |
# See: https://www.electron.build/code-signing
if [ $IS_FORK = true ]; then
echo "Skipping the app signing: building from a fork."
if [ $CAN_SIGN = false ]; then
echo "Skipping the app signing: certificate not provided."
else
if [ "${{ runner.OS }}" = "macOS" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
elif [ "${{ runner.OS }}" = "Windows" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
npm config set msvs_version 2017 --global
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
fi
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
fi
if [ "${{ runner.OS }}" = "Windows" ]; then
npm config set msvs_version 2017 --global
fi
npx node-gyp install
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
@@ -189,7 +216,7 @@ jobs:
release:
needs: changelog
if: github.repository == 'arduino/arduino-ide' && startsWith(github.ref, 'refs/tags/')
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
@@ -214,6 +241,7 @@ jobs:
body: ${{ needs.changelog.outputs.BODY }}
- name: Publish Release [S3]
if: github.repository == 'arduino/arduino-ide'
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'

View File

@@ -1,5 +1,9 @@
name: Check Internationalization
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
push:
@@ -31,6 +35,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -32,6 +32,7 @@ jobs:
- name: Create full changelog
id: full-changelog
run: |
yarn add @octokit/rest --ignore-workspace-root-check
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
# Get the changelog file name to build

View File

@@ -1,5 +1,9 @@
name: i18n-nightly-push
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
on:
schedule:
# run every day at 1AM
@@ -18,6 +22,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -1,5 +1,9 @@
name: i18n-weekly-pull
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
on:
schedule:
# run every monday at 2AM
@@ -18,6 +22,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -0,0 +1,62 @@
name: themes-weekly-pull
on:
schedule:
# run every friday at 5AM
- cron: '0 5 * * 5'
workflow_dispatch:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
NODE_VERSION: 14.x
jobs:
pull-from-jsonbin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn
- name: Run themes:pull script
run: yarn run themes:pull
env:
JSONBIN_MASTER_KEY: ${{ secrets.JSONBIN_MASTER_KEY }}
JSONBIN_ID: ${{ secrets.JSONBIN_ID }}
- name: Generate dark tokens
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/dark.json core,ide-default,ide-dark,theia core,ide-default,ide-dark
- name: Generate default tokens
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/default.json core,ide-default,theia core,ide-default
- name: Run themes:generate script
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated themes
title: Update themes
branch: themes/themes-update
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

6
.gitignore vendored
View File

@@ -17,3 +17,9 @@ yarn*.log
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces

View File

@@ -2,5 +2,6 @@
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 80
"printWidth": 80,
"endOfLine": "auto"
}

70
.vscode/launch.json vendored
View File

@@ -1,6 +1,43 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "App (Electron) [Dev]",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
"--open-devtools"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
@@ -10,7 +47,6 @@
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"protocol": "inspector",
"args": [
".",
"--log-level=debug",
@@ -47,38 +83,6 @@
{
"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",
"protocol": "inspector",
"name": "Run Test [current]",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [

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,145 +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/theia-ide/theia/blob/master/doc/) 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
```
### 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

@@ -62,6 +62,15 @@ The Config Service knows about your system, like for example the default sketch
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol`
### Update **clangd** and **ClangFormat**
The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangFormat** code formatter](https://clang.llvm.org/docs/ClangFormat.html) tool dependencies are managed in parallel. Updating them to a different version is done by the following procedure:
1. If the target version is not already [available from the `arduino/clang-static-binaries` repository](https://github.com/arduino/clang-static-binaries/releases), submit [an issue there](https://github.com/arduino/clang-static-binaries/issues) requesting a build and wait for that to be completed.
1. Validate the **ClangFormat** configuration for the target version by following the instructions [**here**](https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration#clangformat-version-updates)
1. Submit a pull request in the `arduino/arduino-ide` repository to update the version in the `arduino.clangd.version` key of [`package.json`](package.json).
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,15 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc5",
"version": "2.0.0",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-serial-plotter && yarn clean && yarn download-examples && yarn build && yarn test",
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
"clean": "rimraf lib",
"compose-changelog": "node ./scripts/compose-changelog.js",
"download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js",
"copy-serial-plotter": "npx ncp ../node_modules/arduino-serial-plotter-webapp ./build/arduino-serial-plotter-webapp",
"copy-i18n": "npx ncp ../i18n ./build/i18n",
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
@@ -20,31 +20,30 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.3.7",
"@theia/application-package": "1.22.1",
"@theia/core": "1.22.1",
"@theia/editor": "1.22.1",
"@theia/editor-preview": "1.22.1",
"@theia/electron": "1.22.1",
"@theia/filesystem": "1.22.1",
"@theia/git": "1.22.1",
"@theia/keymaps": "1.22.1",
"@theia/markers": "1.22.1",
"@theia/monaco": "1.22.1",
"@theia/navigator": "1.22.1",
"@theia/outline-view": "1.22.1",
"@theia/output": "1.22.1",
"@theia/preferences": "1.22.1",
"@theia/search-in-workspace": "1.22.1",
"@theia/terminal": "1.22.1",
"@theia/workspace": "1.22.1",
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^5.0.35",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
@@ -57,19 +56,19 @@
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.0.17",
"arduino-serial-plotter-webapp": "0.1.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"css-element-queries": "^1.2.0",
"classnames": "^2.3.1",
"dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fuzzy": "^0.1.3",
"fast-safe-stringify": "^2.1.1",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"grpc": "^1.24.11",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
@@ -90,6 +89,7 @@
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"url": "^0.11.0",
@@ -147,25 +147,27 @@
"frontend": "lib/browser/arduino-ide-frontend-module"
},
{
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
"electronMain": "lib/electron-main/arduino-electron-main-module"
}
],
"arduino": {
"cli": {
"version": "0.21.0"
"version": "0.27.1"
},
"fwuploader": {
"version": "2.0.0"
"version": "2.2.0"
},
"clangd": {
"version": "13.0.0"
"version": "14.0.0"
},
"languageServer": {
"version": "0.6.0"
"version": "0.7.1"
}
}
}
}

View File

@@ -1,141 +1,87 @@
// @ts-check
(async () => {
const path = require('path');
const shell = require('shelljs');
const semver = require('semver');
const moment = require('moment');
const downloader = require('./downloader');
const { taskBuildFromGit } = require('./utils');
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
const semver = require('semver');
const moment = require('moment');
const downloader = require('./downloader');
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
return undefined;
}
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
return undefined;
const { arduino } = pkg;
if (!arduino) {
return undefined;
}
const { cli } = arduino;
if (!cli) {
return undefined;
}
const { version } = cli;
return version;
})();
if (!version) {
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
shell.exit(1);
}
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
const destinationPath = path.join(buildFolder, cliName);
if (typeof version === 'string') {
const suffix = (() => {
switch (platform) {
case 'darwin':
return 'macOS_64bit.tar.gz';
case 'win32':
return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm':
return 'Linux_ARMv7.tar.gz';
case 'arm64':
return 'Linux_ARM64.tar.gz';
case 'x64':
return 'Linux_64bit.tar.gz';
default:
return undefined;
}
}
const { arduino } = pkg;
if (!arduino) {
return undefined;
}
const { cli } = arduino;
if (!cli) {
return undefined;
}
const { version } = cli;
return version;
default:
return undefined;
}
})();
if (!version) {
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
shell.exit(1);
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
const destinationPath = path.join(buildFolder, cliName);
if (typeof version === 'string') {
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm': return 'Linux_ARMv7.tar.gz';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
default: return undefined;
}
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
shell.echo(`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
shell.echo(`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else {
shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
shell.echo(
`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
shell.echo(
`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else {
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(`Building CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
if (fs.existsSync(destinationPath)) {
shell.echo(`Skipping the CLI build because it already exists: ${destinationPath}`);
return;
}
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning CLI source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned CLI repo.')
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the CLI...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< CLI build done.')
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
shell.exit(1);
}
const builtCliPath = path.join(tempRepoPath, cliName);
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the CLI.`);
shell.echo('<<< Verifying CLI...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified CLI.');
shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1);
}
} else {
taskBuildFromGit(version, destinationPath, 'CLI');
}
})();

View File

@@ -4,30 +4,93 @@
const version = '1.9.1';
(async () => {
const os = require('os');
const { promises: fs } = require('fs');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
process.exit(1);
if (
shell.exec(
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
).code !== 0
) {
shell.exit(1);
}
if (
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
.code !== 0
) {
shell.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
const isSketch = async (pathLike) => {
try {
const names = await fs.readdir(pathLike);
const dirName = path.basename(pathLike);
return names.indexOf(`${dirName}.ino`) !== -1;
} catch (e) {
if (e.code === 'ENOTDIR') {
return false;
}
throw e;
}
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
shell.exit(1);
process.exit(1);
};
const examples = [];
const categories = await fs.readdir(destination);
const visit = async (pathLike, container) => {
const stat = await fs.lstat(pathLike);
if (stat.isDirectory()) {
if (await isSketch(pathLike)) {
container.sketches.push({
name: path.basename(pathLike),
relativePath: path.relative(destination, pathLike),
});
} else {
const names = await fs.readdir(pathLike);
for (const name of names) {
const childPath = path.join(pathLike, name);
if (await isSketch(childPath)) {
container.sketches.push({
name,
relativePath: path.relative(destination, childPath),
});
} else {
const child = {
label: name,
children: [],
sketches: [],
};
container.children.push(child);
await visit(childPath, child);
}
}
}
}
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
process.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
};
for (const category of categories) {
const example = {
label: category,
children: [],
sketches: [],
};
await visit(path.join(destination, category), example);
examples.push(example);
}
await fs.writeFile(
path.join(destination, 'examples.json'),
JSON.stringify(examples, null, 2),
{ encoding: 'utf8' }
);
shell.echo(`Generated output to ${path.join(destination, 'examples.json')}`);
})();

View File

@@ -7,22 +7,23 @@
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const { goBuildFromGit } = require('./utils');
const [DEFAULT_ALS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) return undefined;
if (!pkg) return [undefined, undefined];
const { arduino } = pkg;
if (!arduino) return undefined;
if (!arduino) return [undefined, undefined];
const { languageServer, clangd } = arduino;
if (!languageServer) return undefined;
if (!clangd) return undefined;
if (!languageServer) return [undefined, undefined];
if (!clangd) return [undefined, undefined];
return [languageServer.version, clangd.version];
})();
if (!DEFAULT_ALS_VERSION) {
if (!DEFAULT_LS_VERSION) {
shell.echo(
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
);
@@ -39,8 +40,8 @@
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`,
default: DEFAULT_LS_VERSION,
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_LS_VERSION}.`,
})
.option('clangd-version', {
alias: 'cv',
@@ -56,7 +57,7 @@
.version(false)
.parse();
const alsVersion = yargs['ls-version'];
const lsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version'];
const force = yargs['force-download'];
const { platform, arch } = process;
@@ -66,24 +67,29 @@
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, lsSuffix, clangdSuffix;
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
default:
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
}
if (!lsSuffix || !clangdSuffix) {
shell.echo(
@@ -92,15 +98,30 @@
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${
alsVersion === 'nightly'
? 'nightly/arduino-language-server'
: 'arduino-language-server_' + alsVersion
}_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
if (typeof lsVersion === 'string') {
const lsUrl = `https://downloads.arduino.cc/arduino-language-server/${
lsVersion === 'nightly'
? 'nightly/arduino-language-server'
: 'arduino-language-server_' + lsVersion
}_${lsSuffix}`;
downloader.downloadUnzipAll(lsUrl, build, lsExecutablePath, force);
} else {
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
}
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
clangFormatExecutablePath,
force,
{
strip: 1,
}
);
})();

View File

@@ -86,6 +86,7 @@ exports.downloadUnzipFile = async (
* @param targetDir {string} Directory into which to decompress the archive
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
* @param decompressOptions {import('decompress').DecompressOptions}
*/
exports.downloadUnzipAll = async (
url,

View File

@@ -0,0 +1,110 @@
/**
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
*
* @param version {object} the version object.
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
*/
exports.taskBuildFromGit = (version, destinationPath, taskName) => {
return buildFromGit('task', version, destinationPath, taskName);
};
/**
* Clones something from GitHub and builds it with `Golang`.
*
* @param version {object} the version object.
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
*/
exports.goBuildFromGit = (version, destinationPath, taskName) => {
return buildFromGit('go', version, destinationPath, taskName);
};
/**
* The `command` is either `go` or `task`.
*/
function buildFromGit(command, version, destinationPath, taskName) {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
// We assume an object with `owner`, `repo`, commitish?` properties.
if (typeof version !== 'object') {
shell.echo(
`Expected a \`{ owner, repo, commitish }\` object. Got <${version}> instead.`
);
}
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(
`Building ${taskName} from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
`Skipping the ${taskName} build because it already exists: ${destinationPath}`
);
return;
}
const buildFolder = path.join(__dirname, '..', 'build');
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Cloned ${taskName} repo.`);
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the ${taskName}...`);
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Done ${taskName} build.`);
const binName = path.basename(destinationPath);
if (!fs.existsSync(path.join(tempRepoPath, binName))) {
shell.echo(
`Could not find the ${taskName} at ${path.join(tempRepoPath, binName)}.`
);
shell.exit(1);
}
const binPath = path.join(tempRepoPath, binName);
shell.echo(
`>>> Copying ${taskName} from ${binPath} to ${destinationPath}...`
);
if (shell.cp(binPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the ${taskName}.`);
shell.echo(`<<< Verifying ${taskName}...`);
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo(`>>> Verified ${taskName}.`);
}

View File

@@ -1,21 +0,0 @@
import { Command } from '@theia/core/lib/common/command';
/**
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
*/
export namespace ArduinoCommands {
export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
id: 'arduino-toggle-compile-for-debug',
};
/**
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
*/
export const OPEN_SKETCH_FILES: Command = {
id: 'arduino-open-sketch-files',
};
export const OPEN_BOARDS_DIALOG: Command = {
id: 'arduino-open-boards-dialog',
};
}

View File

@@ -1,29 +1,19 @@
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import {
BoardsService,
SketchesService,
ExecutableService,
Sketch,
LibraryService,
} from '../common/protocol';
import { Mutex } from 'async-mutex';
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
ILogger,
DisposableCollection,
} from '@theia/core';
import {
FrontendApplication,
FrontendApplicationContribution,
LocalStorageService,
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
@@ -31,48 +21,24 @@ import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { nls } from '@theia/core/lib/common';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import {
EditorMainMenu,
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ConfigService } from '../common/protocol/config-service';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { ArduinoPreferences } from './arduino-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { EditorMode } from './editor-mode';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
import { IDEUpdater } from '../common/protocol/ide-updater';
const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
export const SKIP_IDE_VERSION = 'skipIDEVersion';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
@injectable()
export class ArduinoFrontendContribution
@@ -81,118 +47,25 @@ export class ArduinoFrontendContribution
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution {
@inject(ILogger)
protected logger: ILogger;
ColorContribution
{
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
private readonly messageService: MessageService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(FileService)
protected readonly fileService: FileService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SketchbookWidgetContribution)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(ExecutableService)
protected executableService: ExecutableService;
private readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
protected readonly appStateService: FrontendApplicationStateService;
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(IDEUpdater)
protected readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
protected readonly updaterDialog: IDEUpdaterDialog;
protected invalidConfigPopup:
| Promise<void | 'No' | 'Yes' | undefined>
| undefined;
protected toDisposeOnStop = new DisposableCollection();
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@postConstruct()
protected async init(): Promise<void> {
const isFirstStartup = !(await this.localStorageService.getData(
INIT_LIBS_AND_PACKAGES
));
if (isFirstStartup) {
await this.localStorageService.setData(INIT_LIBS_AND_PACKAGES, true);
const avrPackage = await this.boardsService.getBoardPackage({
id: 'arduino:avr',
});
const builtInLibrary = (
await this.libraryService.search({
query: 'Arduino_BuiltIn',
})
)[0];
!!avrPackage && (await this.boardsService.install({ item: avrPackage }));
!!builtInLibrary &&
(await this.libraryService.install({
item: builtInLibrary,
installDependencies: true,
}));
}
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
@@ -202,232 +75,32 @@ export class ArduinoFrontendContribution
)
);
}
const updateStatusBar = ({
selectedBoard,
selectedPort,
}: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
? `$(microchip) ${selectedBoard.name}`
: `$(close) ${nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
)}`,
className: 'arduino-selected-board',
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
)
: nls.localize('arduino/common/notConnected', '[not connected]'),
className: 'arduino-selected-port',
});
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(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) {
if (
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, {
mode: 'open',
});
}
}
}
})
);
}
});
}
async onStart(app: FrontendApplication): Promise<void> {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
this.updater
.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorageService.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
})
.catch((e) => {
this.messageService.error(
nls.localize(
'arduino/ide-updater/errorCheckingForUpdates',
'Error while checking for Arduino IDE updates.\n{0}',
e.message
)
);
});
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
this.startLanguageServer(fqbn, name);
}
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
onStart(app: FrontendApplication): void {
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'arduino.language.log':
start(this.boardsServiceClientImpl.boardsConfig);
break;
case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
break;
case 'arduino.ide.updateChannel':
case 'arduino.ide.updateBaseUrl':
this.updater.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
);
break;
}
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(
fqbn: string,
name: string | undefined
): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(
`Could not start language server for ${fqbn}. The core is not installed for the board.`
this.appStateService.reachedState('ready').then(() =>
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get(
'arduino.window.zoomLevel'
);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand(
'arduino.languageserver.stop'
);
console.info(
`Stopped language server process for ${this.languageServerFqbn}.`
);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(
`Failed to start language server process for ${this.languageServerFqbn}`,
e
);
throw e;
}
}
return;
}
if (fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
}
}
const { clangdUri, lsUri } = await this.executableService.list();
const [clangdPath, lsPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(lsUri)),
]);
const config = await this.configService.getConfiguration();
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandRegistry.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${config.daemon.port}`, // TODO: verify if this port is coming from the BE
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn,
name: name ? `"${name}"` : undefined,
},
}
),
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
webContents.setZoomLevel(zoomLevel);
})
);
// Removes the _Settings_ (cog) icon from the left sidebar
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -437,13 +110,21 @@ export class ArduinoFrontendContribution
<BoardsToolBarItem
key="boardsToolbarItem"
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl}
boardsServiceProvider={this.boardsServiceProvider}
/>
),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7,
});
registry.registerItem({
id: 'toggle-serial-plotter',
command: SerialPlotterContribution.Commands.OPEN_TOOLBAR.id,
tooltip: nls.localize(
'arduino/serial/openSerialPlotter',
'Serial Plotter'
),
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
@@ -452,26 +133,20 @@ export class ArduinoFrontendContribution
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug,
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
},
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
},
});
for (const command of [
EditorCommands.SPLIT_EDITOR_DOWN,
EditorCommands.SPLIT_EDITOR_LEFT,
EditorCommands.SPLIT_EDITOR_RIGHT,
EditorCommands.SPLIT_EDITOR_UP,
EditorCommands.SPLIT_EDITOR_VERTICAL,
EditorCommands.SPLIT_EDITOR_HORIZONTAL,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR,
]) {
registry.unregisterCommand(command);
}
}
registerMenus(registry: MenuModelRegistry) {
registerMenus(registry: MenuModelRegistry): void {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
@@ -490,97 +165,12 @@ export class ArduinoFrontendContribution
ArduinoMenus.TOOLS,
nls.localize('arduino/menu/tools', 'Tools')
);
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: nls.localize(
'arduino/debug/optimizeForDebugging',
'Optimize for Debugging'
),
order: '5',
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(
'arduino/common/oldFormat',
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
sketch.name
);
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
this.messageService
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
.then(async (answer) => {
if (answer === yes) {
this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async ensureOpened(
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'The primary branding color, such as dialog titles, library, and board manager list labels.',
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'Secondary branding color for list selections, dropdowns, and widget borders.',
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background',
},
description:
'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.',
},
{
id: 'arduino.toolbar.background',
id: 'arduino.toolbar.button.background',
defaults: {
dark: 'button.background',
light: 'button.background',
@@ -590,15 +180,35 @@ export class ArduinoFrontendContribution
'Background color of the toolbar items. Such as Upload, Verify, etc.',
},
{
id: 'arduino.toolbar.hoverBackground',
id: 'arduino.toolbar.button.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.foreground',
hc: 'textLink.foreground',
light: 'button.hoverBackground',
hc: 'button.background',
},
description:
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
},
{
id: 'arduino.toolbar.button.secondary.label',
defaults: {
dark: 'secondaryButton.foreground',
light: 'button.foreground',
hc: 'activityBar.inactiveForeground',
},
description:
'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter',
},
{
id: 'arduino.toolbar.button.secondary.hoverBackground',
defaults: {
dark: 'secondaryButton.hoverBackground',
light: 'button.hoverBackground',
hc: 'textLink.foreground',
},
description:
'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"',
},
{
id: 'arduino.toolbar.toggleBackground',
defaults: {
@@ -610,22 +220,72 @@ export class ArduinoFrontendContribution
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
},
{
id: 'arduino.output.foreground',
id: 'arduino.toolbar.dropdown.border',
defaults: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground',
dark: 'dropdown.border',
light: 'dropdown.border',
hc: 'dropdown.border',
},
description: 'Color of the text in the Output view.',
description: 'Border color of the Board Selector.',
},
{
id: 'arduino.toolbar.dropdown.borderActive',
defaults: {
dark: 'focusBorder',
light: 'focusBorder',
hc: 'focusBorder',
},
description: "Border color of the Board Selector when it's active",
},
{
id: 'arduino.toolbar.dropdown.background',
defaults: {
dark: 'tab.unfocusedActiveBackground',
light: 'dropdown.background',
hc: 'dropdown.background',
},
description: 'Background color of the Board Selector.',
},
{
id: 'arduino.toolbar.dropdown.label',
defaults: {
dark: 'dropdown.foreground',
light: 'dropdown.foreground',
hc: 'dropdown.foreground',
},
description: 'Font color of the Board Selector.',
},
{
id: 'arduino.output.background',
id: 'arduino.toolbar.dropdown.iconSelected',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background',
dark: 'list.activeSelectionIconForeground',
light: 'list.activeSelectionIconForeground',
hc: 'list.activeSelectionIconForeground',
},
description: 'Background color of the Output view.',
description:
'Color of the selected protocol icon in the Board Selector.',
},
{
id: 'arduino.toolbar.dropdown.option.backgroundHover',
defaults: {
dark: 'list.hoverBackground',
light: 'list.hoverBackground',
hc: 'list.hoverBackground',
},
description: 'Background color on hover of the Board Selector options.',
},
{
id: 'arduino.toolbar.dropdown.option.backgroundSelected',
defaults: {
dark: 'list.activeSelectionBackground',
light: 'list.activeSelectionBackground',
hc: 'list.activeSelectionBackground',
},
description:
'Background color of the selected board in the Board Selector.',
}
);
}

View File

@@ -1,5 +1,5 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from 'inversify';
import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -42,22 +42,25 @@ import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@th
import { KeymapsFrontendContribution } from './theia/keymaps/keymaps-frontend-contribution';
import { KeymapsFrontendContribution as TheiaKeymapsFrontendContribution } from '@theia/keymaps/lib/browser/keymaps-frontend-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { EditorPreviewContribution } from './theia/editor/editor-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry,
DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import { ApplicationShell } from './theia/core/application-shell';
import {
ApplicationShell,
DockPanelRenderer,
} from './theia/core/application-shell';
import { FrontendApplication } from './theia/core/frontend-application';
import {
BoardsConfigDialog,
@@ -69,30 +72,24 @@ import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { SerialServiceClientImpl } from './serial/serial-service-client-impl';
import {
SerialServicePath,
SerialService,
SerialServiceClient,
} from '../common/protocol/serial-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { SerialConnectionManager } from './serial/serial-connection-manager';
import { SerialModel } from './serial/serial-model';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
import { ProblemManager } from './theia/markers/problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import {
MonacoThemeJson,
MonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import {
ArduinoDaemonPath,
ArduinoDaemon,
@@ -108,7 +105,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,
@@ -138,15 +136,12 @@ import { PreferencesContribution } from './theia/preferences/preferences-contrib
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { KeybindingRegistry } from './theia/core/keybindings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget';
import { BurnBootloader } from './contributions/burn-bootloader';
import {
ExamplesServicePath,
@@ -160,13 +155,20 @@ import {
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
OutputChannelRegistryMainImpl,
} from './theia/plugin-ext/output-channel-registry-main';
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import {
ExecutableService,
ExecutableServicePath,
MonitorManagerProxy,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
MonitorManagerProxyPath,
} from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
import {
ResponseService,
ResponseServiceArduino,
ResponseServiceClient,
ResponseServicePath,
} from '../common/protocol/response-service';
import { NotificationCenter } from './notification-center';
@@ -212,7 +214,10 @@ import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-
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 { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import {
MonacoEditorFactory,
MonacoEditorProvider as TheiaMonacoEditorProvider,
} from '@theia/monaco/lib/browser/monaco-editor-provider';
import { StorageWrapper } from './storage-wrapper';
import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@@ -275,20 +280,91 @@ import {
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './contributions/startup-task';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer';
import { OpenSketchFiles } from './contributions/open-sketch-files';
import { InoLanguage } from './contributions/ino-language';
import { SelectedBoard } from './contributions/selected-board';
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
import { OpenBoardsConfig } from './contributions/open-boards-config';
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
import { MonacoThemeServiceIsReady } from './utils/window';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { StatusBarImpl } from './theia/core/status-bar';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
import { EditorMenuContribution } from './theia/editor/editor-file';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
import { PreferencesEditorWidget } from './theia/preferences/preference-editor-widget';
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
import {
BoardsFilterRenderer,
LibraryFilterRenderer,
} 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';
const ElementQueries = require('css-element-queries/src/ElementQueries');
MonacoThemingService.register({
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/arduino.color-theme.json'),
});
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
{
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/default.color-theme.json'),
},
{
id: 'arduino-theme-dark',
label: 'Dark (Arduino)',
uiTheme: 'vs-dark',
json: require('../../src/browser/data/dark.color-theme.json'),
},
];
themes.forEach((theme) => MonacoThemingService.register(theme));
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = window as any;
const ready = global[MonacoThemeServiceIsReady] as Deferred;
if (ready) {
ready.promise.then(registerArduinoThemes);
} else {
registerArduinoThemes();
}
export default new ContainerModule((bind, unbind, isBound, rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
@@ -302,6 +378,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Renderer for both the library and the core widgets.
bind(ListItemRenderer).toSelf().inSingletonScope();
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
// Library service
bind(LibraryService)
@@ -358,6 +436,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)
@@ -393,7 +472,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
title: nls.localize(
'arduino/board/boardConfigDialogTitle',
'Select Other Board and Port'
),
});
// Core service
@@ -405,31 +487,47 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(CoreErrorHandler).toSelf().inSingletonScope();
// Serial monitor
bind(SerialModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SerialModel);
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
}));
// Frontend binding for the serial service
bind(SerialService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get<SerialServiceClient>(
SerialServiceClient
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
return connection.createProxy(SerialServicePath, client);
})
.inSingletonScope();
bind(SerialConnectionManager).toSelf().inSingletonScope();
},
}));
// Serial service client to receive and delegate notifications from the backend.
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
bind(MonitorManagerProxyFactory).toFactory(
(context) => () =>
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
);
bind(MonitorManagerProxy)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
MonitorManagerProxyPath,
context.container.get(MonitorManagerProxyClient)
)
)
.inSingletonScope();
// Monitor manager proxy client to receive and delegate pluggable monitors
// notifications from the backend
bind(MonitorManagerProxyClient)
.to(MonitorManagerProxyClientImpl)
.inSingletonScope();
bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
@@ -438,9 +536,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
WorkspaceVariableContribution
);
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
@@ -453,9 +556,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaKeymapsFrontendContribution)
.to(KeymapsFrontendContribution)
.inSingletonScope();
rebind(TheiaEditorPreviewContribution)
.to(EditorPreviewContribution)
.inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaMonacoStatusBarContribution)
.to(MonacoStatusBarContribution)
.inSingletonScope();
@@ -477,7 +578,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaPreferencesContribution)
.to(PreferencesContribution)
.inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution)
.to(WorkspaceCommandContribution)
.inSingletonScope();
@@ -486,14 +586,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) => () => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -508,6 +607,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
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);
// replace search icon
rebind(TheiaSearchInWorkspaceFactory)
.to(SearchInWorkspaceFactory)
@@ -550,6 +653,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
// To remove `New Window` from the `File` menu
bind(WindowContribution).toSelf().inSingletonScope();
rebind(TheiaWindowContribution).toService(WindowContribution);
// To remove `File` > `Close Editor`.
bind(EditorMenuContribution).toSelf().inSingletonScope();
rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution);
// To disable the highlighting of non-unicode characters in the _Output_ view
bind(OutputEditorFactory).toSelf().inSingletonScope();
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
bind(ArduinoDaemon)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -559,6 +675,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
.inSingletonScope();
bind(Formatter)
.toDynamicValue(({ container }) =>
WebSocketConnectionProvider.createProxy(container, FormatterPath)
)
.inSingletonScope();
bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -626,6 +748,30 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
Contribution.configure(bind, StartupTasks);
Contribution.configure(bind, IndexesUpdateProgress);
Contribution.configure(bind, Daemon);
Contribution.configure(bind, FirstStartupInstaller);
Contribution.configure(bind, OpenSketchFiles);
Contribution.configure(bind, InoLanguage);
Contribution.configure(bind, SelectedBoard);
Contribution.configure(bind, CheckForIDEUpdates);
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, DeleteSketch);
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.
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoFormattingConflictsContribution).toService(
MonacoFormattingConflictsContribution
);
bind(ResponseServiceImpl)
.toSelf()
@@ -640,7 +786,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
});
bind(ResponseService).toService(ResponseServiceImpl);
bind(ResponseServiceArduino).toService(ResponseServiceImpl);
bind(ResponseServiceClient).toService(ResponseServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);
@@ -655,15 +801,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Enable the dirty indicator on uncloseable widgets.
rebind(TabBarRendererFactory).toFactory((context) => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(
ContextMenuRenderer
);
const contextMenuRenderer =
context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(
TabBarDecoratorService
);
const iconThemeService = context.container.get<IconThemeService>(
IconThemeService
);
const iconThemeService =
context.container.get<IconThemeService>(IconThemeService);
return new TabBarRenderer(
contextMenuRenderer,
decoratorService,
@@ -673,6 +817,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
@@ -680,6 +826,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
// Do not fetch the `catalog.json` from Azure on FE load.
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
rebind(TheiaDefaultJsonSchemaContribution).toService(
DefaultJsonSchemaContribution
);
// Do not block the app startup when initializing the editor navigation history.
bind(EditorNavigationContribution).toSelf().inSingletonScope();
rebind(TheiaEditorNavigationContribution).toService(
EditorNavigationContribution
);
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
@@ -692,6 +858,30 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// To avoid running a status bar update on every single `keypress` event from the editor.
bind(StatusBarImpl).toSelf().inSingletonScope();
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
// Debounced update for the tab-bar toolbar when typing in the editor.
bind(DockPanelRenderer).toSelf();
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
// Avoid running the "reset scroll" interval tasks until the preference editor opens.
rebind(PreferencesWidget)
.toDynamicValue(({ container }) => {
const child = createPreferencesWidgetContainer(container);
child.bind(PreferencesEditorWidget).toSelf().inSingletonScope();
child
.rebind(TheiaPreferencesEditorWidget)
.toService(PreferencesEditorWidget);
return child.get(PreferencesWidget);
})
.inSingletonScope();
// Preferences
bindArduinoPreferences(bind);
@@ -799,4 +989,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
})
.inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
});

View File

@@ -1,4 +1,4 @@
import { interfaces } from 'inversify';
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
@@ -13,6 +13,32 @@ export enum UpdateChannel {
Stable = 'stable',
Nightly = 'nightly',
}
export const ErrorRevealStrategyLiterals = [
/**
* Scroll vertically as necessary and reveal a line.
*/
'auto',
/**
* Scroll vertically as necessary and reveal a line centered vertically.
*/
'center',
/**
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
*/
'top',
/**
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
}
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
@@ -25,6 +51,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.language.realTimeDiagnostics': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.realTimeDiagnostics',
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
),
default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: nls.localize(
@@ -33,6 +67,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/compile.revealRange',
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
@@ -83,7 +134,7 @@ export const ArduinoConfigSchema: PreferenceSchema = {
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
`The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'`
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
@@ -134,10 +185,10 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
'arduino.cloud.sketchSyncEndpoint': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sketchSyncEnpoint',
'arduino/preferences/cloud.sketchSyncEndpoint',
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
),
default: 'https://api2.arduino.cc/create',
@@ -174,12 +225,39 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
'arduino.checkForUpdates': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/checkForUpdate',
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
),
default: true,
},
},
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.language.realTimeDiagnostics': boolean;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
@@ -193,11 +271,14 @@ export interface ArduinoConfiguration {
'arduino.cloud.pull.warn': boolean;
'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEnpoint': string;
'arduino.cloud.sketchSyncEndpoint': string;
'arduino.auth.clientID': string;
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.checkForUpdates': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@@ -43,13 +43,15 @@ export class AuthenticationClientService
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
onStart(): void {
async onStart(): Promise<void> {
this.toDispose.push(this.onSessionDidChangeEmitter);
this.service.setClient(this);
this.service
.session()
.then((session) => this.notifySessionDidChange(session));
this.setOptions();
this.setOptions().then(() => this.service.initAuthSession());
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName.startsWith('arduino.auth.')) {
this.setOptions();
@@ -57,8 +59,8 @@ export class AuthenticationClientService
});
}
setOptions(): void {
this.service.setOptions({
setOptions(): Promise<void> {
return this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'],

View File

@@ -1,23 +1,42 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
BoardsService,
BoardsPackage,
Board,
Port,
} from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsConfig } from './boards-config';
import { Installable, ResponseServiceArduino } from '../../common/protocol';
import { Installable, ResponseServiceClient } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { nls } from '@theia/core/lib/common';
import { NotificationCenter } from '../notification-center';
import { InstallManually } from '../../common/nls';
interface AutoInstallPromptAction {
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
isAcceptance?: boolean;
key: string;
handler: (...args: unknown[]) => unknown;
}
type AutoInstallPromptActions = AutoInstallPromptAction[];
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
// * Cases in which we do not show the auto-install prompt:
// 1. When a related platform is already installed
// 2. When a prompt is already showing in the UI
// 3. When a board is unplugged
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(MessageService)
protected readonly messageService: MessageService;
@@ -27,8 +46,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
@@ -36,97 +55,227 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = [];
// * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
// we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
// an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
// showing again
private portSelectedOnLastRefusal: Port | undefined;
private lastRefusedPackageId: string | undefined;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(
this.ensureCoreExists.bind(this)
);
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
const setEventListeners = () => {
this.boardsServiceClient.onBoardsConfigChanged((config) => {
const { selectedBoard, selectedPort } = config;
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard, selectedPort } = config;
if (
selectedBoard &&
selectedPort &&
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
) {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => {
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some((board) => board.name === selectedBoard.name)
);
const boardWasUnplugged =
!selectedPort && this.portSelectedOnLastRefusal;
this.clearLastRefusedPromptInfo();
// check if one of the packages for the board is already installed. if so, no hint
if (
packagesForBoard.some(({ installedVersion }) => !!installedVersion)
boardWasUnplugged ||
!selectedBoard ||
this.promptAlreadyShowingForBoard(selectedBoard)
) {
return;
}
// filter the installable (not installed) packages,
// CLI returns the packages already sorted with the deprecated ones at the end of the list
// in order to ensure the new ones are preferred
const candidates = packagesForBoard.filter(
({ installable, installedVersion }) =>
installable && !installedVersion
);
this.ensureCoreExists(selectedBoard, selectedPort);
});
const candidate = candidates[0];
if (candidate) {
const version = candidate.availableVersions[0]
? `[v ${candidate.availableVersions[0]}]`
: '';
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const manualInstall = nls.localize(
'arduino/board/installManually',
'Install Manually'
);
// tslint:disable-next-line:max-line-length
this.messageService
.info(
nls.localize(
'arduino/board/installNow',
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
candidate.name,
version,
selectedBoard.name
),
manualInstall,
yes
)
.then(async (answer) => {
const index = this.notifications.findIndex((board) =>
Board.sameAs(board, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
}
if (answer === yes) {
await Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0],
});
return;
}
if (answer === manualInstall) {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh(candidate.name.toLocaleLowerCase())
);
}
});
// we "clearRefusedPackageInfo" if a "refused" package is eventually
// installed, though this is not strictly necessary. It's more of a
// cleanup, to ensure the related variables are representative of
// current state.
this.notificationCenter.onPlatformDidInstall((installed) => {
if (this.lastRefusedPackageId === installed.item.id) {
this.clearLastRefusedPromptInfo();
}
});
};
// we should invoke this.ensureCoreExists only once we're sure
// everything has been reconciled
this.boardsServiceClient.reconciled.then(() => {
const { selectedBoard, selectedPort } =
this.boardsServiceClient.boardsConfig;
if (selectedBoard) {
this.ensureCoreExists(selectedBoard, selectedPort);
}
setEventListeners();
});
}
private removeNotificationByBoard(selectedBoard: Board): void {
const index = this.notifications.findIndex((notification) =>
Board.sameAs(notification, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
}
}
private clearLastRefusedPromptInfo(): void {
this.lastRefusedPackageId = undefined;
this.portSelectedOnLastRefusal = undefined;
}
private setLastRefusedPromptInfo(
packageId: string,
selectedPort?: Port
): void {
this.lastRefusedPackageId = packageId;
this.portSelectedOnLastRefusal = selectedPort;
}
private promptAlreadyShowingForBoard(board: Board): boolean {
return Boolean(
this.notifications.find((notification) =>
Board.sameAs(notification, board)
)
);
}
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => {
const candidate = this.getInstallCandidate(packages, selectedBoard);
if (candidate) {
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
} else {
this.removeNotificationByBoard(selectedBoard);
}
});
}
private getInstallCandidate(
packages: BoardsPackage[],
selectedBoard: Board
): BoardsPackage | undefined {
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some((board) => board.name === selectedBoard.name)
);
// check if one of the packages for the board is already installed. if so, no hint
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
return;
}
// filter the installable (not installed) packages,
// CLI returns the packages already sorted with the deprecated ones at the end of the list
// in order to ensure the new ones are preferred
const candidates = packagesForBoard.filter(
({ installable, installedVersion }) => installable && !installedVersion
);
return candidates[0];
}
private showAutoInstallPrompt(
candidate: BoardsPackage,
selectedBoard: Board,
selectedPort?: Port
): void {
const candidateName = candidate.name;
const version = candidate.availableVersions[0]
? `[v ${candidate.availableVersions[0]}]`
: '';
const info = this.generatePromptInfoText(
candidateName,
version,
selectedBoard.name
);
const actions = this.createPromptActions(candidate);
const onRefuse = () => {
this.setLastRefusedPromptInfo(candidate.id, selectedPort);
};
const handleAction = this.createOnAnswerHandler(actions, onRefuse);
const onAnswer = (answer: string) => {
this.removeNotificationByBoard(selectedBoard);
handleAction(answer);
};
this.messageService
.info(info, ...actions.map((action) => action.key))
.then(onAnswer);
}
private generatePromptInfoText(
candidateName: string,
version: string,
boardName: string
): string {
return nls.localize(
'arduino/board/installNow',
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
candidateName,
version,
boardName
);
}
private createPromptActions(
candidate: BoardsPackage
): AutoInstallPromptActions {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const actions: AutoInstallPromptActions = [
{
key: InstallManually,
handler: () => {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh({
query: candidate.name.toLocaleLowerCase(),
type: 'All',
})
);
},
},
{
isAcceptance: true,
key: yes,
handler: () => {
return Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0],
});
},
},
];
return actions;
}
private createOnAnswerHandler(
actions: AutoInstallPromptActions,
onRefuse?: () => void
): (answer: string) => void {
return (answer) => {
const actionToHandle = actions.find((action) => action.key === answer);
actionToHandle?.handler();
if (!actionToHandle?.isAcceptance && onRefuse) {
onRefuse();
}
};
}
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
@@ -55,12 +55,13 @@ export class BoardsConfigDialogWidget extends ReactWidget {
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
/>
</div>
);
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();

View File

@@ -1,5 +1,9 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
@@ -26,9 +30,9 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
constructor(
@inject(BoardsConfigDialogProps)
protected readonly props: BoardsConfigDialogProps
protected override readonly props: BoardsConfigDialogProps
) {
super(props);
super({ ...props, maxWidth: 500 });
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
@@ -52,7 +56,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
async open(
override async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
@@ -65,14 +69,6 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = nls.localize(
'arduino/board/configDialogTitle',
'Select Other Board & Port'
);
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
@@ -95,7 +91,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
return head;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -110,23 +106,23 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
this.update();
}
protected onUpdateRequest(msg: Message) {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
@@ -16,6 +16,7 @@ import {
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
@@ -29,6 +30,7 @@ export namespace BoardsConfig {
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
@@ -47,7 +49,7 @@ export abstract class Item<T> extends React.Component<{
missing?: boolean;
details?: string;
}> {
render(): React.ReactNode {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
@@ -99,15 +101,19 @@ export class BoardsConfig extends React.Component<
};
}
componentDidMount() {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
this.props.onAppStateDidChange((state) => {
if (state === 'ready') {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
}
}),
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
this.updatePorts(
event.newState.ports,
AttachedBoardsChangeEvent.diff(event).detached.ports
@@ -120,19 +126,19 @@ export class BoardsConfig extends React.Component<
);
}
),
this.props.notificationCenter.onPlatformInstalled(() =>
this.props.notificationCenter.onPlatformDidInstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformUninstalled(() =>
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdated(() =>
this.props.notificationCenter.onIndexDidUpdate(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStarted(() =>
this.props.notificationCenter.onDaemonDidStart(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStopped(() =>
this.props.notificationCenter.onDaemonDidStop(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
@@ -141,11 +147,11 @@ export class BoardsConfig extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
@@ -250,16 +256,16 @@ export class BoardsConfig extends React.Component<
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<div className="body">
<>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
</>
);
}
@@ -300,7 +306,10 @@ export class BoardsConfig extends React.Component<
type="search"
value={query}
className="theia-input"
placeholder="SEARCH BOARD"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
@@ -328,27 +337,19 @@ export class BoardsConfig extends React.Component<
if (this.state.showAllPorts) {
ports = this.state.knownPorts;
} else {
ports = this.state.knownPorts.filter((port) => {
if (port.protocol === 'serial') {
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of this.availableBoards) {
if (board.port?.address === port.address) {
return true;
}
}
});
ports = this.state.knownPorts.filter(
Port.visiblePorts(this.availableBoards)
);
}
return !ports.length ? (
<div className="loading noselect">No ports discovered</div>
<div className="loading noselect">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{ports.map((port) => (
<Item<Port>
key={`${port.id}`}
key={`${Port.keyOf(port)}`}
item={port}
label={Port.toString(port)}
selected={Port.sameAs(this.state.selectedPort, port)}
@@ -412,53 +413,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

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -13,6 +13,7 @@ import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@@ -31,11 +32,20 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.appStateService
.reachedState('ready')
.then(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsDataStore.onChanged(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard

View File

@@ -1,4 +1,4 @@
import { injectable, inject, named } from 'inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { Event, Emitter } from '@theia/core/lib/common/event';
@@ -33,7 +33,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
let shouldFireChanged = false;
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)

View File

@@ -1,21 +1,27 @@
import { inject, injectable, postConstruct } from 'inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import {
BoardSearch,
BoardsPackage,
BoardsService,
} from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardsPackage> {
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<BoardsPackage>
@inject(BoardsService) service: BoardsService,
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
) {
super({
id: BoardsListWidget.WIDGET_ID,
@@ -26,23 +32,25 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All' },
});
}
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() =>
this.notificationCenter.onPlatformDidInstall(() =>
this.refresh(undefined)
),
this.notificationCenter.onPlatformUninstalled(() =>
this.notificationCenter.onPlatformDidUninstall(() =>
this.refresh(undefined)
),
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -63,7 +71,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
);
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {

View File

@@ -1,7 +1,12 @@
import { injectable, inject } from 'inversify';
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';
@@ -13,16 +18,28 @@ import {
AttachedBoardsChangeEvent,
BoardWithPackage,
BoardUserField,
AvailablePorts,
} from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
import { NotificationCenter } from '../notification-center';
import { ArduinoCommands } from '../arduino-commands';
import { StorageWrapper } from '../storage-wrapper';
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;
@@ -38,12 +55,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
@inject(NotificationCenter)
protected notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly onBoardsConfigChangedEmitter =
new Emitter<BoardsConfig.Config>();
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -61,11 +82,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected _availablePorts: Port[] = [];
protected _availableBoards: AvailableBoard[] = [];
private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined;
private lastAvailablePortsOnUpload: Port[] | undefined;
private boardConfigToAutoSelect: BoardsConfig.Config | undefined;
/**
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
* This even also fires, when the boards package was not available for the currently selected board,
* Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\
* This event also fires, when the boards package was not available for the currently selected board,
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
* currently selected board.\
* currently selected board.
*
* This event is also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
@@ -73,29 +99,126 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
this.onAvailableBoardsChangedEmitter.event;
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
private readonly _reconciled = new Deferred<void>();
onStart(): void {
this.notificationCenter.onAttachedBoardsChanged(
this.notificationCenter.onAttachedBoardsDidChange(
this.notifyAttachedBoardsChanged.bind(this)
);
this.notificationCenter.onPlatformInstalled(
this.notificationCenter.onPlatformDidInstall(
this.notifyPlatformInstalled.bind(this)
);
this.notificationCenter.onPlatformUninstalled(
this.notificationCenter.onPlatformDidUninstall(
this.notifyPlatformUninstalled.bind(this)
);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts(),
this.loadState(),
]).then(([attachedBoards, availablePorts]) => {
this.appStateService.reachedState('ready').then(async () => {
const [state] = await Promise.all([
this.boardsService.getState(),
this.loadState(),
]);
const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state);
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
await this.reconcileAvailableBoards();
this.tryReconnect();
this._reconciled.resolve();
});
}
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;
}
snapshotBoardDiscoveryOnUpload(): void {
this.lastBoardsConfigOnUpload = this._boardsConfig;
this.lastAvailablePortsOnUpload = this._availablePorts;
}
clearBoardDiscoverySnapshot(): void {
this.lastBoardsConfigOnUpload = undefined;
this.lastAvailablePortsOnUpload = undefined;
}
private portToAutoSelectCanBeDerived(): boolean {
return Boolean(
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
);
}
attemptPostUploadAutoSelect(): void {
setTimeout(() => {
if (this.portToAutoSelectCanBeDerived()) {
this.attemptAutoSelect({
ports: this._availablePorts,
boards: this._availableBoards,
});
}
}, 2000); // 2 second delay same as IDE 1.8
}
private attemptAutoSelect(
newState: AttachedBoardsChangeEvent['newState']
): void {
this.deriveBoardConfigToAutoSelect(newState);
this.tryReconnect();
}
private deriveBoardConfigToAutoSelect(
newState: AttachedBoardsChangeEvent['newState']
): void {
if (!this.portToAutoSelectCanBeDerived()) {
this.boardConfigToAutoSelect = undefined;
return;
}
const oldPorts = this.lastAvailablePortsOnUpload!;
const { ports: newPorts, boards: newBoards } = newState;
const appearedPorts =
oldPorts.length > 0
? newPorts.filter((newPort: Port) =>
oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort))
)
: newPorts;
for (const port of appearedPorts) {
const boardOnAppearedPort = newBoards.find((board: Board) =>
Port.sameAs(board.port, port)
);
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
if (
boardOnAppearedPort &&
lastBoardsConfigOnUpload.selectedBoard &&
Board.sameAs(
boardOnAppearedPort,
lastBoardsConfigOnUpload.selectedBoard
)
) {
this.clearBoardDiscoverySnapshot();
this.boardConfigToAutoSelect = {
selectedBoard: boardOnAppearedPort,
selectedPort: port,
};
return;
}
}
}
protected notifyAttachedBoardsChanged(
event: AttachedBoardsChangeEvent
): void {
@@ -104,10 +227,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
this.logger.info(AttachedBoardsChangeEvent.toString(event));
this.logger.info('------------------------------------------');
}
this._attachedBoards = event.newState.boards;
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an
// upload is in progress
if (!uploadInProgress) {
this.attemptAutoSelect(event.newState);
}
});
}
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
@@ -155,7 +286,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
.then(async (answer) => {
if (answer === yes) {
this.commandService.executeCommand(
ArduinoCommands.OPEN_BOARDS_DIALOG.id,
'arduino-open-boards-dialog',
selectedBoard.name
);
}
@@ -185,8 +316,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
? selectedBoard
: this._availableBoards.find((availableBoard) =>
Board.sameAs(availableBoard, selectedBoard)
);
Board.sameAs(availableBoard, selectedBoard)
);
if (
selectedAvailableBoard &&
selectedAvailableBoard.selected &&
@@ -209,7 +340,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
}
protected async tryReconnect(): Promise<boolean> {
protected tryReconnect(): boolean {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of this.availableBoards.filter(
({ state }) => state !== AvailableBoard.State.incomplete
@@ -223,23 +354,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of this.availableBoards.filter(
({ state }) => state !== AvailableBoard.State.incomplete
)) {
if (
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
this.latestValidBoardsConfig.selectedPort.protocol === board.port?.protocol
) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: board.port,
};
return true;
}
}
if (!this.boardConfigToAutoSelect) return false;
this.boardsConfig = this.boardConfigToAutoSelect;
this.boardConfigToAutoSelect = undefined;
return true;
}
return false;
}
@@ -258,7 +378,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
protected setBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this.logger.debug('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.latestBoardsConfig = this._boardsConfig;
if (this.canUploadTo(this._boardsConfig)) {
@@ -364,6 +484,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return this._availableBoards;
}
/**
* @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload.
*
* See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236.
*/
// TODO: remove this API and fix the selected board config store/restore correctly.
get availablePorts(): Port[] {
return this._availablePorts.slice();
}
async waitUntilAvailable(
what: Board & { port: Port },
timeout?: number
@@ -376,14 +506,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const timeoutTask =
!!timeout && timeout > 0
? new Promise<void>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
)
)
)
: new Promise<void>(() => {
/* never */
});
/* never */
});
const waitUntilTask = new Promise<void>((resolve) => {
let candidate = find(what, this.availableBoards);
if (candidate) {
@@ -420,28 +550,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const currentAvailableBoards = this._availableBoards;
const availableBoards: AvailableBoard[] = [];
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
const availableBoardPorts = availablePorts.filter((port) => {
if (port.protocol === 'serial') {
// We always show all serial ports, even if there
// is no recognized board connected to it
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of attachedBoards) {
if (board.port?.address === port.address) {
return true;
}
}
return false;
});
const availableBoardPorts = availablePorts.filter(
Port.visiblePorts(attachedBoards)
);
for (const boardPort of availableBoardPorts) {
const board = attachedBoards.find(({ port }) =>
Port.sameAs(boardPort, port)
);
// "board" will always be falsey for
// port that was originally mapped
// to unknown board and then selected
// manually by user
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
boardPort
);
@@ -460,12 +581,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
availableBoard = {
...lastSelectedBoard,
state: AvailableBoard.State.guessed,
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
selected:
BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard) &&
Port.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection
port: boardPort,
};
} else {
availableBoard = {
name: nls.localize('arduino/common/unknown', 'Unknown'),
name: Unknown,
port: boardPort,
state: AvailableBoard.State.incomplete,
};
@@ -475,8 +598,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
if (
boardsConfig.selectedBoard &&
!availableBoards.some(({ selected }) => selected)
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.
@@ -484,11 +608,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,
});
@@ -500,6 +628,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
for (let i = 0; !hasChanged && i < availableBoards.length; i++) {
const [left, right] = [availableBoards[i], currentAvailableBoards[i]];
hasChanged =
left.fqbn !== right.fqbn ||
!!AvailableBoard.compare(left, right) ||
left.selected !== right.selected;
}
@@ -534,8 +663,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${typeof port === 'string' ? port : port.address
}`;
return `last-selected-board-on-port:${
typeof port === 'string' ? port : port.address
}`;
}
protected async loadState(): Promise<void> {
@@ -552,11 +682,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;
@@ -579,8 +712,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,15 +1,16 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands';
import { OpenBoardsConfig } from '../contributions/open-boards-config';
import {
BoardsServiceProvider,
AvailableBoard,
} from './boards-service-provider';
import { nls } from '@theia/core/lib/common';
import classNames from 'classnames';
import { BoardsConfig } from './boards-config';
export interface BoardsDropDownListCoords {
readonly top: number;
@@ -28,10 +29,12 @@ export namespace BoardsDropDown {
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement;
private listRef: React.RefObject<HTMLDivElement>;
constructor(props: BoardsDropDown.Props) {
super(props);
this.listRef = React.createRef();
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
@@ -41,7 +44,13 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
}
}
render(): React.ReactNode {
override componentDidUpdate(prevProps: BoardsDropDown.Props): void {
if (prevProps.coords === 'hidden' && this.listRef.current) {
this.listRef.current.focus();
}
}
override render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
@@ -61,21 +70,22 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
position: 'absolute',
...coords,
}}
ref={this.listRef}
tabIndex={0}
>
{items
.map(({ name, port, selected, onClick }) => ({
label: nls.localize(
'arduino/board/boardListItem',
'{0} at {1}',
name,
Port.toString(port)
),
selected,
onClick,
}))
.map(this.renderItem)}
<div className="arduino-boards-dropdown-list--items-container">
{items
.map(({ name, port, selected, onClick }) => ({
boardLabel: name,
port,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
<div
key={footerLabel}
tabIndex={0}
className="arduino-boards-dropdown-item arduino-board-dropdown-footer"
onClick={() => this.props.openBoardsConfig()}
>
@@ -86,22 +96,52 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
}
protected renderItem({
label,
boardLabel,
port,
selected,
onClick,
}: {
label: string;
boardLabel: string;
port: Port;
selected?: boolean;
onClick: () => void;
}): React.ReactNode {
const protocolIcon = iconNameFromProtocol(port.protocol);
const onKeyUp = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
onClick();
}
};
return (
<div
key={label}
className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`}
key={`board-item--${boardLabel}-${port.address}`}
className={classNames('arduino-boards-dropdown-item', {
'arduino-boards-dropdown-item--selected': selected,
})}
onClick={onClick}
onKeyUp={onKeyUp}
tabIndex={0}
>
<div>{label}</div>
{selected ? <span className="fa fa-check" /> : ''}
<div
className={classNames(
'arduino-boards-dropdown-item--protocol',
'fa',
protocolIcon
)}
/>
<div
className="arduino-boards-dropdown-item--label"
title={`${boardLabel}\n${port.address}`}
>
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
{boardLabel}
</div>
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
{port.addressLabel}
</div>
</div>
{selected ? <div className="fa fa-check" /> : ''}
</div>
);
}
@@ -119,7 +159,7 @@ export class BoardsToolBarItem extends React.Component<
constructor(props: BoardsToolBarItem.Props) {
super(props);
const { availableBoards } = props.boardsServiceClient;
const { availableBoards } = props.boardsServiceProvider;
this.state = {
availableBoards,
coords: 'hidden',
@@ -130,17 +170,17 @@ export class BoardsToolBarItem extends React.Component<
});
}
componentDidMount() {
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
this.setState({ availableBoards })
override componentDidMount(): void {
this.props.boardsServiceProvider.onAvailableBoardsChanged(
(availableBoards) => this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
@@ -161,38 +201,45 @@ export class BoardsToolBarItem extends React.Component<
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
override render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, {
default: nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
),
});
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached';
}
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed';
}
return '';
})();
const { selectedBoard, selectedPort } =
this.props.boardsServiceProvider.boardsConfig;
const boardLabel =
selectedBoard?.name ||
nls.localize('arduino/board/selectBoard', 'Select Board');
const selectedPortLabel = portLabel(selectedPort?.address);
const isConnected = Boolean(selectedBoard && selectedPort);
const protocolIcon = isConnected
? iconNameFromProtocol(selectedPort?.protocol || '')
: null;
const protocolIconClassNames = classNames(
'arduino-boards-toolbar-item--protocol',
'fa',
protocolIcon
);
return (
<React.Fragment>
<div className="arduino-boards-toolbar-item-container">
<div className="arduino-boards-toolbar-item" title={title}>
<div className="inner-container" onClick={this.show}>
<span className={decorator} />
<div className="label noWrapInfo">
<div className="noWrapInfo noselect">{title}</div>
</div>
<span className="fa fa-caret-down caret" />
</div>
<div
className="arduino-boards-toolbar-item-container"
title={selectedPortLabel}
onClick={this.show}
>
{protocolIcon && <div className={protocolIconClassNames} />}
<div
className={classNames(
'arduino-boards-toolbar-item--label',
'noWrapInfo',
'noselect',
{ 'arduino-boards-toolbar-item--label-connected': isConnected }
)}
>
{boardLabel}
</div>
<div className="fa fa-caret-down caret" />
</div>
<BoardsDropDown
coords={coords}
@@ -201,17 +248,20 @@ export class BoardsToolBarItem extends React.Component<
.map((board) => ({
...board,
onClick: () => {
if (board.state === AvailableBoard.State.incomplete) {
this.props.boardsServiceClient.boardsConfig = {
if (!board.fqbn) {
const previousBoardConfig =
this.props.boardsServiceProvider.boardsConfig;
this.props.boardsServiceProvider.boardsConfig = {
selectedPort: board.port,
};
this.openDialog();
this.openDialog(previousBoardConfig);
} else {
this.props.boardsServiceClient.boardsConfig = {
this.props.boardsServiceProvider.boardsConfig = {
selectedBoard: board,
selectedPort: board.port,
};
}
this.setState({ coords: 'hidden' });
},
}))}
openBoardsConfig={this.openDialog}
@@ -220,14 +270,25 @@ export class BoardsToolBarItem extends React.Component<
);
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
protected openDialog = async (
previousBoardConfig?: BoardsConfig.Config
): Promise<void> => {
const selectedBoardConfig =
await this.props.commands.executeCommand<BoardsConfig.Config>(
OpenBoardsConfig.Commands.OPEN_DIALOG.id
);
if (
previousBoardConfig &&
(!selectedBoardConfig?.selectedPort ||
!selectedBoardConfig?.selectedBoard)
) {
this.props.boardsServiceProvider.boardsConfig = previousBoardConfig;
}
};
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardsServiceClient: BoardsServiceProvider;
readonly boardsServiceProvider: BoardsServiceProvider;
readonly commands: CommandRegistry;
}
@@ -236,3 +297,26 @@ export namespace BoardsToolBarItem {
coords: BoardsDropDownListCoords | 'hidden';
}
}
function iconNameFromProtocol(protocol: string): string {
switch (protocol) {
case 'serial':
return 'fa-arduino-technology-usb';
case 'network':
return 'fa-arduino-technology-connection';
/*
Bluetooth ports are not listed yet from the CLI;
Not sure about the naming ('bluetooth'); make sure it's correct before uncommenting the following lines
*/
// case 'bluetooth':
// return 'fa-arduino-technology-bluetooth';
default:
return 'fa-arduino-technology-3dimensionscube';
}
}
function portLabel(portName?: string): string {
return portName
? nls.localize('arduino/board/portLabel', 'Port: {0}', portName)
: nls.localize('arduino/board/disconnected', 'Disconnected');
}

View File

@@ -1,10 +1,16 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { BoardsListWidget } from './boards-list-widget';
import { BoardsPackage } from '../../common/protocol/boards-service';
import type {
BoardSearch,
BoardsPackage,
} from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
BoardsPackage,
BoardSearch
> {
constructor() {
super({
widgetId: BoardsListWidget.WIDGET_ID,
@@ -18,7 +24,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
});
}
async initializeLayout(): Promise<void> {
override async initializeLayout(): Promise<void> {
this.openView();
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as moment from 'moment';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
@@ -22,13 +22,13 @@ export class About extends Contribution {
@inject(ConfigService)
protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id,
label: nls.localize(

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
@@ -10,19 +10,20 @@ import {
} from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id,
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
@@ -32,7 +33,7 @@ export class AddFile extends SketchContribution {
protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({

View File

@@ -1,14 +1,11 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Installable,
LibraryService,
ResponseServiceArduino,
} from '../../common/protocol';
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import {
SketchContribution,
Command,
@@ -22,27 +19,23 @@ export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),
@@ -92,7 +85,7 @@ export class AddZipLibrary extends SketchContribution {
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
try {
await Installable.doWithProgress({
await ExecuteWithProgress.doWithProgress({
messageService: this.messageService,
progressText:
nls.localize('arduino/common/processing', 'Processing') +

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
@@ -10,16 +10,17 @@ import {
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
@@ -32,7 +33,7 @@ export class ArchiveSketch extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const archiveBasename = `${sketch.name}-${dateFormat(

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -47,7 +47,7 @@ export class BoardSelection extends SketchContribution {
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } =
@@ -100,19 +100,20 @@ PID: ${PID}`;
});
}
onStart(): void {
override onStart(): void {
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus());
this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus());
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onAvailablePortsChanged(() =>
this.updateMenus()
);
}
override async onReady(): Promise<void> {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailablePortsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {
@@ -287,7 +288,7 @@ PID: ${PID}`;
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
const [port, boards] = ports[portID];
let label = `${port.address}`;
let label = `${port.addressLabel}`;
if (boards.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
@@ -330,7 +331,7 @@ PID: ${PID}`;
}
};
const grouped = AvailablePorts.byProtocol(availablePorts);
const grouped = AvailablePorts.groupByProtocol(availablePorts);
let protocolOrder = 100;
// We first show serial and network ports, then all the rest
['serial', 'network'].forEach((protocol) => {

View File

@@ -1,42 +1,23 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { nls } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
Command,
CommandRegistry,
CoreServiceContribution,
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
export class BurnBootloader extends CoreServiceContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: nls.localize(
@@ -47,26 +28,20 @@ export class BurnBootloader extends SketchContribution {
});
}
async burnBootloader(): Promise<void> {
private async burnBootloader(): Promise<void> {
this.clearVisibleNotification();
const options = await this.options();
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
programmer,
port,
verify,
verbose,
await this.doWithProgress({
progressText: nls.localize(
'arduino/bootloader/burningBootloader',
'Burning bootloader...'
),
task: (progressId, coreService) =>
coreService.burnBootloader({
...options,
progressId,
}),
});
this.messageService.info(
nls.localize(
@@ -78,17 +53,30 @@ export class BurnBootloader extends SketchContribution {
}
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
await this.serialConnection.reconnectAfterUpload();
this.handleError(e);
}
}
private async options(): Promise<CoreService.Options.Bootloader> {
const { boardsConfig } = this.boardsServiceProvider;
const port = boardsConfig.selectedPort;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
return {
fqbn,
programmer,
port,
verify,
verbose,
};
}
}
export namespace BurnBootloader {

View File

@@ -0,0 +1,68 @@
import { nls } from '@theia/core/lib/common/nls';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
IDEUpdater,
SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution';
@injectable()
export class CheckForIDEUpdates extends Contribution {
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
override onStart(): void {
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (newValue !== oldValue) {
switch (preferenceName) {
case 'arduino.ide.updateChannel':
case 'arduino.ide.updateBaseUrl':
this.updater.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
);
}
}
}
);
}
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(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
})
.catch((e) => {
this.messageService.error(
nls.localize(
'arduino/ide-updater/errorCheckingForUpdates',
'Error while checking for Arduino IDE updates.\n{0}',
e.message
)
);
});
}
}

View File

@@ -0,0 +1,221 @@
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { InstallManually, Later } from '../../common/nls';
import {
ArduinoComponent,
BoardsPackage,
BoardsService,
LibraryPackage,
LibraryService,
ResponseServiceClient,
Searchable,
} from '../../common/protocol';
import { Installable } from '../../common/protocol/installable';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import type { ListWidget } from '../widgets/component-list/list-widget';
import { Command, CommandRegistry, Contribution } from './contribution';
const NoUpdates = nls.localize(
'arduino/checkForUpdates/noUpdates',
'There are no recent updates available.'
);
const PromptUpdateBoards = nls.localize(
'arduino/checkForUpdates/promptUpdateBoards',
'Updates are available for some of your boards.'
);
const PromptUpdateLibraries = nls.localize(
'arduino/checkForUpdates/promptUpdateLibraries',
'Updates are available for some of your libraries.'
);
const UpdatingBoards = nls.localize(
'arduino/checkForUpdates/updatingBoards',
'Updating boards...'
);
const UpdatingLibraries = nls.localize(
'arduino/checkForUpdates/updatingLibraries',
'Updating libraries...'
);
const InstallAll = nls.localize(
'arduino/checkForUpdates/installAll',
'Install All'
);
interface Task<T extends ArduinoComponent> {
readonly run: () => Promise<void>;
readonly item: T;
}
const Updatable = { type: 'Updatable' } as const;
@injectable()
export class CheckForUpdates extends Contribution {
@inject(WindowServiceExt)
private readonly windowService: WindowServiceExt;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(LibraryService)
private readonly libraryService: LibraryService;
@inject(BoardsListWidgetFrontendContribution)
private readonly boardsContribution: BoardsListWidgetFrontendContribution;
@inject(LibraryListWidgetFrontendContribution)
private readonly librariesContribution: LibraryListWidgetFrontendContribution;
override registerCommands(register: CommandRegistry): void {
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
execute: () => this.checkForUpdates(false),
});
}
override async onReady(): Promise<void> {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (checkForUpdates) {
this.windowService.isFirstWindow().then((firstWindow) => {
if (firstWindow) {
this.checkForUpdates();
}
});
}
}
private async checkForUpdates(silent = true) {
const [boardsPackages, libraryPackages] = await Promise.all([
this.boardsService.search(Updatable),
this.libraryService.search(Updatable),
]);
this.promptUpdateBoards(boardsPackages);
this.promptUpdateLibraries(libraryPackages);
if (!libraryPackages.length && !boardsPackages.length && !silent) {
this.messageService.info(NoUpdates);
}
}
private promptUpdateBoards(items: BoardsPackage[]): void {
this.prompt({
items,
installable: this.boardsService,
viewContribution: this.boardsContribution,
viewSearchOptions: { query: '', ...Updatable },
promptMessage: PromptUpdateBoards,
updatingMessage: UpdatingBoards,
});
}
private promptUpdateLibraries(items: LibraryPackage[]): void {
this.prompt({
items,
installable: this.libraryService,
viewContribution: this.librariesContribution,
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
promptMessage: PromptUpdateLibraries,
updatingMessage: UpdatingLibraries,
});
}
private prompt<
T extends ArduinoComponent,
S extends Searchable.Options
>(options: {
items: T[];
installable: Installable<T>;
viewContribution: AbstractViewContribution<ListWidget<T, S>>;
viewSearchOptions: S;
promptMessage: string;
updatingMessage: string;
}): void {
const {
items,
installable,
viewContribution,
promptMessage: message,
viewSearchOptions,
updatingMessage,
} = options;
if (!items.length) {
return;
}
this.messageService
.info(message, Later, InstallManually, InstallAll)
.then((answer) => {
if (answer === InstallAll) {
const tasks = items.map((item) =>
this.createInstallTask(item, installable)
);
this.executeTasks(updatingMessage, tasks);
} else if (answer === InstallManually) {
viewContribution
.openView({ reveal: true })
.then((widget) => widget.refresh(viewSearchOptions));
}
});
}
private async executeTasks(
message: string,
tasks: Task<ArduinoComponent>[]
): Promise<void> {
if (tasks.length) {
return ExecuteWithProgress.withProgress(
message,
this.messageService,
async (progress) => {
try {
const total = tasks.length;
let count = 0;
for (const { run, item } of tasks) {
try {
await run(); // runs update sequentially. // TODO: is parallel update desired?
} catch (err) {
console.error(err);
this.messageService.error(
`Failed to update ${item.name}. ${err}`
);
} finally {
progress.report({ work: { total, done: ++count } });
}
}
} finally {
progress.cancel();
}
}
);
}
}
private createInstallTask<T extends ArduinoComponent>(
item: T,
installable: Installable<T>
): Task<T> {
const latestVersion = item.availableVersions[0];
return {
item,
run: () =>
Installable.installWithProgress({
installable,
item,
version: latestVersion,
messageService: this.messageService,
responseService: this.responseService,
keepOutput: true,
}),
};
}
}
export namespace CheckForUpdates {
export namespace Commands {
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
{
id: 'arduino-check-for-updates',
label: 'Check for Arduino Updates',
category: 'Arduino',
},
'arduino/checkForUpdates/checkForUpdates'
);
}
}

View File

@@ -1,39 +1,42 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { injectable } from '@theia/core/shared/inversify';
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import type { MaybePromise } from '@theia/core/lib/common/types';
import type {
FrontendApplication,
OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application';
import { nls } from '@theia/core/lib/common/nls';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
Sketch,
URI,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch';
/**
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
*/
@injectable()
export class Close extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
private shell: ApplicationShell | undefined;
protected shell: ApplicationShell;
onStart(app: FrontendApplication): void {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.shell = app.shell;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => {
execute: () => {
// Close current editor if closeable.
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) {
@@ -41,72 +44,24 @@ export class Close extends SketchContribution {
return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: [
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.dontSave',
"Don't Save"
),
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.save',
'Save'
),
],
message: nls.localize(
'arduino/common/saveChangesToSketch',
'Do you want to save changes to this sketch before closing?'
),
detail: nls.localize(
'arduino/common/loseChanges',
"If you don't save, your changes will be lost."
),
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
if (this.shell) {
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
}
window.close();
return remote.getCurrentWindow().close();
},
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: nls.localize('vscode/editor.contribution/close', 'Close'),
@@ -114,13 +69,130 @@ export class Close extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',
});
}
// `FrontendApplicationContribution#onWillStop`
onWillStop(): OnWillStopAction {
return {
reason: 'save-sketch',
action: () => {
return this.showSaveSketchDialog();
},
};
}
/**
* If returns with `true`, IDE2 will close. Otherwise, it won't.
*/
private async showSaveSketchDialog(): Promise<boolean> {
const sketch = await this.isCurrentSketchTemp();
if (!sketch) {
// Normal close workflow: if there are dirty editors prompt the user.
if (!this.shell) {
console.error(
`Could not get the application shell. Something went wrong.`
);
return true;
}
if (this.shell.canSaveAll()) {
const prompt = await this.prompt(false);
switch (prompt) {
case Prompt.DoNotSave:
return true;
case Prompt.Cancel:
return false;
case Prompt.Save: {
await this.shell.saveAll();
return true;
}
default:
throw new Error(`Unexpected prompt: ${prompt}`);
}
}
return true;
}
// If non of the sketch files were ever touched, do not prompt the save dialog. (#1274)
const wereTouched = await Promise.all(
Sketch.uris(sketch).map((uri) => this.wasTouched(uri))
);
if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) {
return true;
}
const prompt = await this.prompt(true);
switch (prompt) {
case Prompt.DoNotSave:
return true;
case Prompt.Cancel:
return false;
case Prompt.Save: {
// If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI.
const result = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: false,
wipeOriginal: true,
markAsRecentlyOpened: true,
}
);
return !!result;
}
default:
throw new Error(`Unexpected prompt: ${prompt}`);
}
}
private async prompt(isTemp: boolean): Promise<Prompt> {
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'arduino/sketch/saveSketch',
'Save your sketch to open it again later.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [
nls.localizeByDefault("Don't Save"),
Dialog.CANCEL,
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
],
defaultId: 2, // `Save`/`Save As...` button index is the default.
}
);
switch (response) {
case 0:
return Prompt.DoNotSave;
case 1:
return Prompt.Cancel;
case 2:
return Prompt.Save;
default:
throw new Error(`Unexpected response: ${response}`);
}
}
private async isCurrentSketchTemp(): Promise<false | Sketch> {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
const isTemp = await this.sketchService.isTemp(currentSketch);
if (isTemp) {
return currentSketch;
}
}
return false;
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
@@ -130,13 +202,23 @@ export class Close extends SketchContribution {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor.getControl().getModel()?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
if (this.isInteger(versionId) && versionId > 1) {
return true;
}
}
}
return false;
}
private isInteger(arg: unknown): arg is number {
return Number.isInteger(arg);
}
}
enum Prompt {
Save,
DoNotSave,
Cancel,
}
export namespace Close {

View File

@@ -0,0 +1,804 @@
import {
Command,
CommandRegistry,
Disposable,
DisposableCollection,
Emitter,
MaybeArray,
MaybePromise,
nls,
notEmpty,
} from '@theia/core';
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Location,
Range,
} from '@theia/core/shared/vscode-languageserver-protocol';
import {
EditorWidget,
TextDocumentChangeEvent,
} from '@theia/editor/lib/browser';
import {
EditorDecoration,
TrackedRangeStickiness,
} from '@theia/editor/lib/browser/decorations/editor-decoration';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import * as monaco from '@theia/monaco-editor-core';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
import { OutputUri } from '@theia/output/lib/common/output-uri';
import { CoreError } from '../../common/protocol/core-service';
import { ErrorRevealStrategy } from '../arduino-preferences';
import { ArduinoOutputSelector, InoSelector } from '../selectors';
import { Contribution } from './contribution';
import { CoreErrorHandler } from './core-error-handler';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
interface ErrorDecorationRef {
/**
* This is the unique ID of the decoration given by `monaco`.
*/
readonly id: string;
/**
* The resource this decoration belongs to.
*/
readonly uri: string;
}
export namespace ErrorDecorationRef {
export function is(arg: unknown): arg is ErrorDecorationRef {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'uri' in object &&
typeof object['uri'] === 'string' &&
'id' in object &&
typeof object['id'] === 'string'
);
}
return false;
}
export function sameAs(
left: ErrorDecorationRef,
right: ErrorDecorationRef
): boolean {
return left.id === right.id && left.uri === right.uri;
}
}
interface ErrorDecoration extends ErrorDecorationRef {
/**
* The range of the error location the error in the compiler output from the CLI.
*/
readonly rangesInOutput: monaco.Range[];
}
namespace ErrorDecoration {
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration
): monaco.Range | undefined;
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration[]
): (monaco.Range | undefined)[];
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration | ErrorDecoration[]
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
if (editorOrModel) {
const allDecorations = getAllDecorations(editorOrModel);
if (allDecorations) {
if (Array.isArray(decorations)) {
return decorations.map(({ id: decorationId }) =>
findRangeOf(decorationId, allDecorations)
);
} else {
return findRangeOf(decorations.id, allDecorations);
}
}
}
return Array.isArray(decorations)
? decorations.map(() => undefined)
: undefined;
}
function findRangeOf(
decorationId: string,
allDecorations: { id: string; range?: monaco.Range }[]
): monaco.Range | undefined {
return allDecorations.find(
({ id: candidateId }) => candidateId === decorationId
)?.range;
}
function getAllDecorations(
editorOrModel: MonacoEditor | ITextModel
): { id: string; range?: monaco.Range }[] {
if (editorOrModel instanceof MonacoEditor) {
const model = editorOrModel.getControl().getModel();
if (!model) {
return [];
}
return model.getAllDecorations();
}
return editorOrModel.getAllDecorations();
}
}
@injectable()
export class CompilerErrors
extends Contribution
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
{
@inject(EditorManager)
private readonly editorManager: EditorManager;
@inject(ProtocolToMonacoConverter)
private readonly p2m: ProtocolToMonacoConverter;
@inject(MonacoToProtocolConverter)
private readonly m2p: MonacoToProtocolConverter;
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
private revealStrategy = ErrorRevealStrategy.Default;
private experimental = false;
private readonly errors: ErrorDecoration[] = [];
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
private readonly onCurrentErrorDidChange =
this.currentErrorDidChangEmitter.event;
private readonly toDisposeOnCompilerErrorDidChange =
new DisposableCollection();
private shell: ApplicationShell | undefined;
private currentError: ErrorDecoration | undefined;
private get currentErrorIndex(): number {
const current = this.currentError;
if (!current) {
return -1;
}
return this.errors.findIndex((error) =>
ErrorDecorationRef.sameAs(error, current)
);
}
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
monaco.languages.registerCodeLensProvider(InoSelector, this);
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
this.handleCompilerErrorsDidChange(errors)
);
this.onCurrentErrorDidChange(async (error) => {
const monacoEditor = await this.monacoEditor(error.uri);
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
if (!monacoRange) {
console.warn(
'compiler-errors',
`Could not find range of decoration: ${error.id}`
);
return;
}
const range = this.m2p.asRange(monacoRange);
const editor = await this.revealLocationInEditor({
uri: error.uri,
range,
});
if (!editor) {
console.warn(
'compiler-errors',
`Failed to mark error ${error.id} as the current one.`
);
} else {
const monacoEditor = this.monacoEditor(editor);
if (monacoEditor) {
monacoEditor.cursor = range.start;
}
}
});
}
override onReady(): MaybePromise<void> {
this.preferences.ready.then(() => {
this.experimental = Boolean(
this.preferences['arduino.compile.experimental']
);
const strategy = this.preferences['arduino.compile.revealRange'];
this.revealStrategy = ErrorRevealStrategy.is(strategy)
? strategy
: ErrorRevealStrategy.Default;
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (newValue === oldValue) {
return;
}
switch (preferenceName) {
case 'arduino.compile.revealRange': {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
return;
}
case 'arduino.compile.experimental': {
this.experimental = Boolean(newValue);
this.onDidChangeEmitter.fire(this);
return;
}
}
}
);
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to next error. Unknown current error.`
);
return;
}
const nextError =
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
return this.markAsCurrentError(nextError, {
forceReselect: true,
reveal: true,
});
},
isEnabled: () =>
this.experimental && !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to previous error. Unknown current error.`
);
return;
}
const previousError =
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
return this.markAsCurrentError(previousError, {
forceReselect: true,
reveal: true,
});
},
isEnabled: () =>
this.experimental && !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
execute: (arg: unknown) => {
if (ErrorDecorationRef.is(arg)) {
return this.markAsCurrentError(
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
{ forceReselect: true, reveal: true }
);
}
},
isEnabled: () => !!this.errors.length,
});
}
get onDidChange(): monaco.IEvent<this> {
return this.onDidChangeEmitter.event;
}
async provideCodeLenses(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.CodeLensList> {
const lenses: monaco.languages.CodeLens[] = [];
if (
this.experimental &&
this.currentError &&
this.currentError.uri === model.uri.toString() &&
this.errors.length > 1
) {
const monacoEditor = await this.monacoEditor(model.uri);
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
if (range) {
lenses.push(
{
range,
command: {
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
title: nls.localize(
'arduino/editor/previousError',
'Previous Error'
),
arguments: [this.currentError],
},
},
{
range,
command: {
id: CompilerErrors.Commands.NEXT_ERROR.id,
title: nls.localize('arduino/editor/nextError', 'Next Error'),
arguments: [this.currentError],
},
}
);
}
}
return {
lenses,
dispose: () => {
/* NOOP */
},
};
}
async provideLinks(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.ILinksList> {
const links: monaco.languages.ILink[] = [];
if (
model.uri.scheme === OutputUri.SCHEME &&
model.uri.path === '/Arduino'
) {
links.push(
...this.errors
.filter((decoration) => !!decoration.rangesInOutput.length)
.map(({ rangesInOutput, id, uri }) =>
rangesInOutput.map(
(range) =>
<monaco.languages.ILink>{
range,
url: monaco.Uri.parse(`command://`).with({
query: JSON.stringify({ id, uri }),
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
}),
tooltip: nls.localize(
'arduino/editor/revealError',
'Reveal Error'
),
}
)
)
.reduce((acc, curr) => acc.concat(curr), [])
);
} else {
console.warn('unexpected URI: ' + model.uri.toString());
}
return { links };
}
async resolveLink(
link: monaco.languages.ILink,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.ILink | undefined> {
if (!this.experimental) {
return undefined;
}
const { url } = link;
if (url) {
const candidateUri = new URI(
typeof url === 'string' ? url : url.toString()
);
const candidateId = candidateUri.path.toString();
const error = this.errors.find((error) => error.id === candidateId);
if (error) {
const monacoEditor = await this.monacoEditor(error.uri);
const range = ErrorDecoration.rangeOf(monacoEditor, error);
if (range) {
return {
range,
url: monaco.Uri.parse(error.uri),
};
}
}
}
return undefined;
}
private async handleCompilerErrorsDidChange(
errors: CoreError.ErrorLocation[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const groupedErrors = this.groupBy(
errors,
(error: CoreError.ErrorLocation) => error.location.uri
);
const decorations = await this.decorateEditors(groupedErrors);
this.errors.push(...decorations.errors);
this.toDisposeOnCompilerErrorDidChange.pushAll([
Disposable.create(() => (this.errors.length = 0)),
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
...(await Promise.all([
decorations.dispose,
this.trackEditors(
groupedErrors,
(editor) =>
editor.onSelectionChanged((selection) =>
this.handleSelectionChange(editor, selection)
),
(editor) =>
editor.onDispose(() =>
this.handleEditorDidDispose(editor.uri.toString())
),
(editor) =>
editor.onDocumentContentChanged((event) =>
this.handleDocumentContentChange(editor, event)
)
),
])),
]);
const currentError = this.errors[0];
if (currentError) {
await this.markAsCurrentError(currentError, {
forceReselect: true,
reveal: true,
});
}
}
private async decorateEditors(
errors: Map<string, CoreError.ErrorLocation[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) =>
this.decorateEditor(uri, errors)
)
);
return {
dispose: new DisposableCollection(
...composite.map(({ dispose }) => dispose)
),
errors: composite.reduce(
(acc, { errors }) => acc.concat(errors),
[] as ErrorDecoration[]
),
};
}
private async decorateEditor(
uri: string,
errors: CoreError.ErrorLocation[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.monacoEditor(uri);
if (!editor) {
return { dispose: Disposable.NULL, errors: [] };
}
const oldDecorations = editor.deltaDecorations({
oldDecorations: [],
newDecorations: errors.map((error) =>
this.compilerErrorDecoration(error.location.range)
),
});
return {
dispose: Disposable.create(() => {
if (editor) {
editor.deltaDecorations({
oldDecorations,
newDecorations: [],
});
}
}),
errors: oldDecorations.map((id, index) => ({
id,
uri,
rangesInOutput: errors[index].rangesInOutput.map((range) =>
this.p2m.asRange(range)
),
})),
};
}
private compilerErrorDecoration(range: Range): EditorDecoration {
return {
range,
options: {
isWholeLine: true,
className: 'compiler-error',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
},
};
}
/**
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
*/
private handleSelectionChange(
monacoEditor: MonacoEditor,
selection: Range
): void {
const uri = monacoEditor.uri.toString();
const monacoSelection = this.p2m.asRange(selection);
console.log(
'compiler-errors',
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
);
const calculatePriority = (
candidateErrorRange: monaco.Range,
currentSelection: monaco.Range
) => {
console.trace(
'compiler-errors',
`Candidate error range: ${candidateErrorRange.toJSON()}`
);
console.trace(
'compiler-errors',
`Current selection range: ${currentSelection.toJSON()}`
);
if (candidateErrorRange.intersectRanges(currentSelection)) {
console.trace('Intersects.');
return { score: 2 };
}
if (
candidateErrorRange.startLineNumber <=
currentSelection.startLineNumber &&
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
) {
console.trace('Same line.');
return { score: 1 };
}
console.trace('No match');
return undefined;
};
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
const rangesPerResource = ErrorDecoration.rangeOf(
monacoEditor,
errorsPerResource
);
const error = rangesPerResource
.map((range, index) => ({ error: errorsPerResource[index], range }))
.map(({ error, range }) => {
if (range) {
const priority = calculatePriority(range, monacoSelection);
if (priority) {
return { ...priority, error };
}
}
return undefined;
})
.filter(notEmpty)
.sort((left, right) => right.score - left.score) // highest first
.map(({ error }) => error)
.shift();
if (error) {
this.markAsCurrentError(error);
} else {
console.info(
'compiler-errors',
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
);
}
}
/**
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
* If editor closes, delete the decorators.
*/
private handleEditorDidDispose(uri: string): void {
let i = this.errors.length;
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
while (i--) {
const error = this.errors[i];
if (error.uri === uri) {
this.errors.splice(i, 1);
}
}
this.onDidChangeEmitter.fire(this);
}
/**
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
*/
private handleDocumentContentChange(
monacoEditor: MonacoEditor,
event: TextDocumentChangeEvent
): void {
const errorsPerResource = this.errors.filter(
(error) => error.uri === event.document.uri
);
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
const doc = event.document;
if (doc instanceof MonacoEditorModel) {
editorOrModel = doc.textEditorModel;
}
const rangesPerResource = ErrorDecoration.rangeOf(
editorOrModel,
errorsPerResource
);
const resolvedDecorations = rangesPerResource.map((range, index) => ({
error: errorsPerResource[index],
range,
}));
const decoratorsToRemove = event.contentChanges
.map(({ range }) => this.p2m.asRange(range))
.map((changedRange) =>
resolvedDecorations
.filter(({ range: decorationRange }) => {
if (!decorationRange) {
return false;
}
const affects =
changedRange.startLineNumber <= decorationRange.startLineNumber &&
changedRange.endLineNumber >= decorationRange.endLineNumber;
console.log(
'compiler-errors',
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
);
return affects;
})
.map(({ error }) => {
const index = this.errors.findIndex((candidate) =>
ErrorDecorationRef.sameAs(candidate, error)
);
return index !== -1 ? { error, index } : undefined;
})
.filter(notEmpty)
)
.reduce((acc, curr) => acc.concat(curr), [])
.sort((left, right) => left.index - right.index); // highest index last
if (decoratorsToRemove.length) {
let i = decoratorsToRemove.length;
while (i--) {
this.errors.splice(decoratorsToRemove[i].index, 1);
}
monacoEditor.getControl().deltaDecorations(
decoratorsToRemove.map(({ error }) => error.id),
[]
);
this.onDidChangeEmitter.fire(this);
}
}
private async trackEditors(
errors: Map<string, CoreError.ErrorLocation[]>,
...track: ((editor: MonacoEditor) => Disposable)[]
): Promise<Disposable> {
return new DisposableCollection(
...(await Promise.all(
Array.from(errors.keys()).map(async (uri) => {
const editor = await this.monacoEditor(uri);
if (!editor) {
return Disposable.NULL;
}
return new DisposableCollection(...track.map((t) => t(editor)));
})
))
);
}
private async markAsCurrentError(
ref: ErrorDecorationRef,
options?: { forceReselect?: boolean; reveal?: boolean }
): Promise<void> {
const index = this.errors.findIndex((candidate) =>
ErrorDecorationRef.sameAs(candidate, ref)
);
if (index < 0) {
console.warn(
'compiler-errors',
`Failed to mark error ${
ref.id
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
({ id }) => id
)}`
);
return;
}
const newError = this.errors[index];
if (
options?.forceReselect ||
!this.currentError ||
!ErrorDecorationRef.sameAs(this.currentError, newError)
) {
this.currentError = this.errors[index];
console.log(
'compiler-errors',
`Current error changed to ${this.currentError.id}`
);
if (options?.reveal) {
this.currentErrorDidChangEmitter.fire(this.currentError);
}
this.onDidChangeEmitter.fire(this);
}
}
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
private async revealLocationInEditor(
location: Location
): Promise<EditorWidget | undefined> {
const { uri, range } = location;
const editor = await this.editorManager.getByUri(new URI(uri), {
mode: 'activate',
});
if (editor && this.shell) {
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
editor.editor.revealRange(range, { at: this.revealStrategy });
const activeWidget = await this.shell.activateWidget(editor.id);
if (!activeWidget) {
console.warn(
'compiler-errors',
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
);
return editor;
}
if (editor !== activeWidget) {
console.warn(
'compiler-errors',
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
);
}
return editor;
}
console.warn(
'compiler-errors',
`could not find editor widget for URI: ${uri}`
);
return undefined;
}
private groupBy<K, V>(
elements: V[],
extractKey: (element: V) => K
): Map<K, V[]> {
return elements.reduce((acc, curr) => {
const key = extractKey(curr);
let values = acc.get(key);
if (!values) {
values = [];
acc.set(key, values);
}
values.push(curr);
return acc;
}, new Map<K, V[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
private monacoEditor(
uri: string | monaco.Uri
): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | monaco.Uri | EditorWidget
): MaybePromise<MonacoEditor | undefined> {
if (uriOrWidget instanceof EditorWidget) {
const editor = uriOrWidget.editor;
if (editor instanceof MonacoEditor) {
return editor;
}
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);
}
return undefined;
});
}
}
}
export namespace CompilerErrors {
export namespace Commands {
export const NEXT_ERROR: Command = {
id: 'arduino-editor-next-error',
};
export const PREVIOUS_ERROR: Command = {
id: 'arduino-editor-previous-error',
};
export const MARK_AS_CURRENT: Command = {
id: 'arduino-editor-mark-as-current-error',
};
}
}

View File

@@ -1,4 +1,9 @@
import { inject, injectable, interfaces } from 'inversify';
import {
inject,
injectable,
interfaces,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Saveable } from '@theia/core/lib/browser/saveable';
@@ -9,7 +14,7 @@ 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 { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
MenuModelRegistry,
MenuContribution,
@@ -32,16 +37,30 @@ import {
CommandContribution,
CommandService,
} from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../dialogs/settings/settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
ResponseServiceClient,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
export {
Command,
@@ -75,24 +94,40 @@ export abstract class Contribution
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
onStart(app: FrontendApplication): MaybePromise<void> {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerCommands(registry: CommandRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerMenus(registry: MenuModelRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerKeybindings(registry: KeybindingRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerToolbarItems(registry: TabBarToolbarRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onReady(): MaybePromise<void> {}
}
@injectable()
@@ -115,9 +150,6 @@ export abstract class SketchContribution extends Contribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@@ -127,7 +159,7 @@ export abstract class SketchContribution extends Contribution {
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
if (CurrentSketch.isValid(sketch)) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
@@ -139,8 +171,113 @@ export abstract class SketchContribution extends Contribution {
}
}
@injectable()
export abstract class CoreServiceContribution extends SketchContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(CoreService)
private readonly coreService: CoreService;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
/**
* This is the internal (Theia) ID of the notification that is currently visible.
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
*/
private visibleNotificationId: string | undefined;
protected clearVisibleNotification(): void {
if (this.visibleNotificationId) {
this.notificationManager.clear(this.visibleNotificationId);
this.visibleNotificationId = undefined;
}
}
protected handleError(error: unknown): void {
this.tryToastErrorMessage(error);
}
private tryToastErrorMessage(error: unknown): void {
let message: undefined | string = undefined;
if (CoreError.is(error)) {
message = error.message;
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
} else {
try {
message = JSON.stringify(error);
} catch {}
}
if (message) {
if (message.includes('Missing FQBN (Fully Qualified Board Name)')) {
message = nls.localize(
'arduino/coreContribution/noBoardSelected',
'No board selected. Please select your Arduino board from the Tools > Board menu.'
);
}
const copyAction = nls.localize(
'arduino/coreContribution/copyError',
'Copy error messages'
);
this.visibleNotificationId = this.notificationId(message, copyAction);
this.messageService.error(message, copyAction).then(async (action) => {
if (action === copyAction) {
const content = await this.outputChannelManager.contentOfChannel(
'Arduino'
);
if (content) {
this.clipboardService.writeText(content);
}
}
});
} else {
throw error;
}
}
protected async doWithProgress<T>(options: {
progressText: string;
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
}): Promise<T> {
const { progressText, keepOutput, task } = options;
this.outputChannelManager
.getChannel('Arduino')
.show({ preserveFocus: true });
const result = await ExecuteWithProgress.doWithProgress({
messageService: this.messageService,
responseService: this.responseService,
progressText,
run: ({ progressId }) => task(progressId, this.coreService),
keepOutput,
});
return result;
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager.getMessageId({
text: message,
actions,
type: MessageType.Error,
});
}
}
export namespace Contribution {
export function configure<T>(
export function configure(
bind: interfaces.Bind,
serviceIdentifier: typeof Contribution
): void {

View File

@@ -0,0 +1,32 @@
import { Emitter, Event } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { CoreError } from '../../common/protocol/core-service';
@injectable()
export class CoreErrorHandler {
private readonly errors: CoreError.ErrorLocation[] = [];
private readonly compilerErrorsDidChangeEmitter = new Emitter<
CoreError.ErrorLocation[]
>();
tryHandle(error: unknown): void {
if (CoreError.is(error)) {
this.errors.length = 0;
this.errors.push(...error.data);
this.fireCompilerErrorsDidChange();
}
}
reset(): void {
this.errors.length = 0;
this.fireCompilerErrorsDidChange();
}
get onCompilerErrorsDidChange(): Event<CoreError.ErrorLocation[]> {
return this.compilerErrorsDidChangeEmitter.event;
}
private fireCompilerErrorsDidChange(): void {
this.compilerErrorsDidChangeEmitter.fire(this.errors.slice());
}
}

View File

@@ -0,0 +1,41 @@
import { nls } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArduinoDaemon } from '../../common/protocol';
import { Contribution, Command, CommandRegistry } from './contribution';
@injectable()
export class Daemon extends Contribution {
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Daemon.Commands.START_DAEMON, {
execute: () => this.daemon.start(),
});
registry.registerCommand(Daemon.Commands.STOP_DAEMON, {
execute: () => this.daemon.stop(),
});
registry.registerCommand(Daemon.Commands.RESTART_DAEMON, {
execute: () => this.daemon.restart(),
});
}
}
export namespace Daemon {
export namespace Commands {
export const START_DAEMON: Command = {
id: 'arduino-start-daemon',
label: nls.localize('arduino/daemon/start', 'Start Daemon'),
category: 'Arduino',
};
export const STOP_DAEMON: Command = {
id: 'arduino-stop-daemon',
label: nls.localize('arduino/daemon/stop', 'Stop Daemon'),
category: 'Arduino',
};
export const RESTART_DAEMON: Command = {
id: 'arduino-restart-daemon',
label: nls.localize('arduino/daemon/restart', 'Restart Daemon'),
category: 'Arduino',
};
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -12,45 +12,53 @@ import {
SketchContribution,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
@injectable()
export class Debug extends SketchContribution {
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
private readonly hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
@inject(ExecutableService)
protected readonly executableService: ExecutableService;
private readonly executableService: ExecutableService;
@inject(BoardsService)
protected readonly boardService: BoardsService;
private readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/
protected _disabledMessages?: string = nls.localize(
private _disabledMessages?: string = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
); // Initial pessimism.
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
protected onDisabledMessageDidChange =
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
private onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
protected get disabledMessage(): string | undefined {
private get disabledMessage(): string | undefined {
return this._disabledMessages;
}
protected set disabledMessage(message: string | undefined) {
private set disabledMessage(message: string | undefined) {
this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
}
protected readonly debugToolbarItem = {
private readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${
@@ -66,7 +74,7 @@ export class Debug extends SketchContribution {
onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
override onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
@@ -79,68 +87,87 @@ export class Debug extends SketchContribution {
: Debug.Commands.START_DEBUGGING.label
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
}
registerCommands(registry: CommandRegistry): void {
override onReady(): MaybePromise<void> {
this.refreshState();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
execute: () => this.startDebug(),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage,
});
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
execute: () => this.toggleCompileForDebug(),
isToggled: () => this.compileForDebug,
});
registry.registerCommand(Debug.Commands.IS_OPTIMIZE_FOR_DEBUG, {
execute: () => this.compileForDebug,
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem(this.debugToolbarItem);
}
protected async startDebug(
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.id,
label: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.label,
order: '5',
});
}
private async refreshState(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
}
private async startDebug(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
@@ -156,7 +183,7 @@ export class Debug extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
@@ -178,8 +205,19 @@ export class Debug extends SketchContribution {
};
return this.commandService.executeCommand('arduino.debug.start', config);
}
}
get compileForDebug(): boolean {
const value = window.localStorage.getItem(COMPILE_FOR_DEBUG_KEY);
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
this.mainMenuManager.update();
}
}
export namespace Debug {
export namespace Commands {
export const START_DEBUGGING = Command.toLocalizedCommand(
@@ -190,5 +228,16 @@ export namespace Debug {
},
'vscode/debug.contribution/startDebuggingHelp'
);
export const TOGGLE_OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand(
{
id: 'arduino-toggle-optimize-for-debug',
label: 'Optimize for Debugging',
category: 'Arduino',
},
'arduino/debug/optimizeForDebugging'
);
export const IS_OPTIMIZE_FOR_DEBUG: Command = {
id: 'arduino-is-optimize-for-debug',
};
}
}

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

@@ -1,7 +1,6 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import {
Contribution,
@@ -12,21 +11,20 @@ import {
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable()
export class EditContributions extends Contribution {
@inject(MonacoEditorService)
protected readonly codeEditorService: MonacoEditorService;
private readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
private readonly clipboardService: ClipboardService;
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
execute: () => this.run('editor.action.gotoLine'),
});
@@ -43,10 +41,10 @@ export class EditContributions extends Contribution {
execute: () => this.run('actions.find'),
});
registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
execute: () => this.run('actions.findWithSelection'),
execute: () => this.run('editor.action.nextMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
execute: () => this.run('editor.action.nextMatchFindAction'),
execute: () => this.run('editor.action.previousMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
@@ -91,7 +89,7 @@ ${value}
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0',
@@ -143,6 +141,11 @@ ${value}
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'),
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
@@ -199,7 +202,7 @@ ${value}
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
@@ -250,10 +253,13 @@ ${value}
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
protected async current(): Promise<
ICodeEditor | StandaloneCodeEditor | undefined
> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
this.codeEditorService.getActiveCodeEditor() ||
undefined
);
}

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import {
MenuPath,
@@ -19,10 +19,11 @@ import {
SketchContribution,
CommandRegistry,
MenuModelRegistry,
URI,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, Sketch, SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export abstract class Examples extends SketchContribution {
@@ -43,8 +44,8 @@ export abstract class Examples extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
@postConstruct()
init(): void {
protected override init(): void {
super.init();
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
@@ -54,7 +55,7 @@ export abstract class Examples extends SketchContribution {
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
@@ -82,7 +83,7 @@ export abstract class Examples extends SketchContribution {
registerRecursively(
sketchContainerOrPlaceholder:
| SketchContainer
| (Sketch | SketchContainer)[]
| (SketchRef | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
@@ -100,7 +101,7 @@ export abstract class Examples extends SketchContribution {
)
);
} else {
const sketches: Sketch[] = [];
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
@@ -150,10 +151,13 @@ export abstract class Examples extends SketchContribution {
return {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
return this.commandService
.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch)
.then((result) => {
const name = new URI(uri).path.base;
this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await
return result;
});
},
};
}
@@ -161,7 +165,7 @@ export abstract class Examples extends SketchContribution {
@injectable()
export class BuiltInExamples extends Examples {
onStart(): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
@@ -201,13 +205,16 @@ export class LibraryExamples extends Examples {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void {
this.register(); // no `await`
this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register());
override onStart(): void {
this.notificationCenter.onLibraryDidInstall(() => this.register());
this.notificationCenter.onLibraryDidUninstall(() => this.register());
}
protected handleBoardChanged(board: Board | undefined): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
protected override handleBoardChanged(board: Board | undefined): void {
this.register(board);
}

View File

@@ -0,0 +1,102 @@
import { LocalStorageService } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
BoardsService,
LibraryLocation,
LibraryService,
} from '../../common/protocol';
import { Contribution } from './contribution';
@injectable()
export class FirstStartupInstaller extends Contribution {
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(LibraryService)
private readonly libraryService: LibraryService;
override async onReady(): Promise<void> {
const isFirstStartup = !(await this.localStorageService.getData(
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES
));
if (isFirstStartup) {
const avrPackage = await this.boardsService.getBoardPackage({
id: 'arduino:avr',
});
const builtInLibrary = (
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
)[0];
let avrPackageError: Error | undefined;
let builtInLibraryError: Error | undefined;
if (avrPackage) {
try {
await this.boardsService.install({
item: avrPackage,
noOverwrite: true, // We don't want to automatically replace custom platforms the user might already have in place
});
} catch (e) {
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/ffe4232b359fcfa87238d68acf1c3b64a1621f14#diff-10ffbdde46838dd9caa881fd1f2a5326a49f8061f6cfd7c9d430b4875a6b6895R62
if (
e.message.includes(
`Platform ${avrPackage.id}@${avrPackage.installedVersion} already installed`
)
) {
// If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
console.error(e);
} else {
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
avrPackageError = e;
}
}
} else {
avrPackageError = new Error('Could not find platform.');
}
if (builtInLibrary) {
try {
await this.libraryService.install({
item: builtInLibrary,
installDependencies: true,
noOverwrite: true, // We don't want to automatically replace custom libraries the user might already have in place
installLocation: LibraryLocation.BUILTIN,
});
} catch (e) {
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/2ea3608453b17b1157f8a1dc892af2e13e40f4f0#diff-1de7569144d4e260f8dde0e0d00a4e2a218c57966d583da1687a70d518986649R95
if (/Library (.*) is already installed/.test(e.message)) {
// If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up
console.log('error installing core', e);
} else {
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
builtInLibraryError = e;
}
}
} else {
builtInLibraryError = new Error('Could not find library');
}
if (avrPackageError) {
this.messageService.error(
`Could not install Arduino AVR platform: ${avrPackageError}`
);
}
if (builtInLibraryError) {
this.messageService.error(
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
);
}
if (!avrPackageError && !builtInLibraryError) {
await this.localStorageService.setData(
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES,
true
);
}
}
}
}
export namespace FirstStartupInstaller {
export const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
}

View File

@@ -0,0 +1,78 @@
import { MaybePromise } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { Formatter } from '../../common/protocol/formatter';
import { InoSelector } from '../selectors';
import { Contribution, URI } from './contribution';
@injectable()
export class Format
extends Contribution
implements
monaco.languages.DocumentRangeFormattingEditProvider,
monaco.languages.DocumentFormattingEditProvider
{
@inject(Formatter)
private readonly formatter: Formatter;
override onStart(): MaybePromise<void> {
monaco.languages.registerDocumentRangeFormattingEditProvider(
InoSelector,
this
);
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
}
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const text = await this.format(model, range, options);
return [{ range, text }];
}
async provideDocumentFormattingEdits(
model: monaco.editor.ITextModel,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = model.getFullModelRange();
const text = await this.format(model, range, options);
return [{ range, text }];
}
/**
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
* folder locations where the `.clang-format` file could be.
*/
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
const editorUri = new URI(model.uri.toString());
return this.workspaceService
.tryGetRoots()
.map(({ resource }) => resource)
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
.map((uri) => uri.toString());
}
private format(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions
): Promise<string> {
console.info(
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
range.toJSON()
)}]`
);
const content = model.getValueInRange(range);
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
return this.formatter.format({
content,
formatterConfigFolderUris,
options,
});
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@@ -15,6 +15,7 @@ import {
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from '../ide-updater/ide-updater-commands';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class Help extends Contribution {
@@ -27,7 +28,7 @@ export class Help extends Contribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
const open = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) =>
@@ -40,7 +41,9 @@ export class Help extends Contribution {
);
registry.registerCommand(
Help.Commands.ENVIRONMENT,
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
createOpenHandler(
'https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
)
);
registry.registerCommand(
Help.Commands.TROUBLESHOOTING,
@@ -91,7 +94,7 @@ export class Help extends Contribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.unregisterMenuAction({
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
});
@@ -135,7 +138,7 @@ export class Help extends Contribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F',

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser';
@@ -16,6 +16,8 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -29,7 +31,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@@ -43,18 +45,21 @@ export class IncludeLibrary extends SketchContribution {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
onStart(): void {
this.updateMenuActions();
override onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions()
);
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
this.notificationCenter.onLibraryUninstalled(() =>
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
this.notificationCenter.onLibraryDidUninstall(() =>
this.updateMenuActions()
);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.updateMenuActions();
}
override registerMenus(registry: MenuModelRegistry): void {
// `Include Library` submenu
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
@@ -77,7 +82,7 @@ export class IncludeLibrary extends SketchContribution {
});
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
@@ -168,7 +173,7 @@ export class IncludeLibrary extends SketchContribution {
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.

View File

@@ -0,0 +1,71 @@
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ProgressMessage } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
import { Contribution } from './contribution';
@injectable()
export class IndexesUpdateProgress extends Contribution {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(ProgressService)
private readonly progressService: ProgressService;
private currentProgress:
| (Progress & Readonly<{ progressId: string }>)
| undefined;
override onStart(): void {
this.notificationCenter.onIndexWillUpdate((progressId) =>
this.getOrCreateProgress(progressId)
);
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
this.getOrCreateProgress(progress).then((delegate) =>
delegate.report(progress)
);
});
this.notificationCenter.onIndexDidUpdate((progressId) => {
this.cancelProgress(progressId);
});
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
this.cancelProgress(progressId);
this.messageService.error(message);
});
}
private async getOrCreateProgress(
progressOrId: ProgressMessage | string
): Promise<Progress & { progressId: string }> {
const progressId = ProgressMessage.is(progressOrId)
? progressOrId.progressId
: progressOrId;
if (this.currentProgress?.progressId === progressId) {
return this.currentProgress;
}
if (this.currentProgress) {
this.currentProgress.cancel();
}
this.currentProgress = undefined;
const progress = await this.progressService.showProgress({
text: '',
options: { location: 'notification' },
});
if (ProgressMessage.is(progressOrId)) {
progress.report(progressOrId);
}
this.currentProgress = { ...progress, progressId };
return this.currentProgress;
}
private cancelProgress(progressId: string) {
if (this.currentProgress) {
if (this.currentProgress.progressId !== progressId) {
console.warn(
`Mismatching progress IDs. Expected ${progressId}, got ${this.currentProgress.progressId}. Canceling anyway.`
);
}
this.currentProgress.cancel();
this.currentProgress = undefined;
}
}
}

View File

@@ -0,0 +1,159 @@
import { Mutex } from 'async-mutex';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
ArduinoDaemon,
BoardsService,
ExecutableService,
} from '../../common/protocol';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { SketchContribution, URI } from './contribution';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
@injectable()
export class InoLanguage extends SketchContribution {
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
private readonly executableService: ExecutableService;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
private languageServerFqbn?: string;
private languageServerStartMutex = new Mutex();
override onReady(): void {
const start = (
{ selectedBoard }: BoardsConfig.Config,
forceStart = false
) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
this.startLanguageServer(fqbn, name, forceStart);
}
}
};
this.boardsServiceProvider.onBoardsConfigChanged(start);
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig)
);
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
);
this.preferences.onPreferenceChanged(
({ preferenceName, oldValue, newValue }) => {
if (oldValue !== newValue) {
switch (preferenceName) {
case 'arduino.language.log':
case 'arduino.language.realTimeDiagnostics':
start(this.boardsServiceProvider.boardsConfig, true);
}
}
}
);
start(this.boardsServiceProvider.boardsConfig);
}
private async startLanguageServer(
fqbn: string,
name: string | undefined,
forceStart = false
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
return;
}
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(
`Could not start language server for ${fqbn}. The core is not installed for the board.`
);
if (this.languageServerFqbn) {
try {
await this.commandService.executeCommand(
'arduino.languageserver.stop'
);
console.info(
`Stopped language server process for ${this.languageServerFqbn}.`
);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(
`Failed to start language server process for ${this.languageServerFqbn}`,
e
);
throw e;
}
}
return;
}
if (!forceStart && fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.preferences.get('arduino.language.log');
const realTimeDiagnostics = this.preferences.get(
'arduino.language.realTimeDiagnostics'
);
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
}
}
const { clangdUri, lsUri } = await this.executableService.list();
const [clangdPath, lsPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(lsUri)),
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandService.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn,
name: name ? `"${name}"` : undefined,
},
realTimeDiagnostics,
silentOutput: true,
}
),
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
}
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
@@ -9,12 +9,11 @@ import {
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
@injectable()
export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(),
});
@@ -25,7 +24,7 @@ export class NewSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New'),
@@ -33,22 +32,13 @@ export class NewSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
tooltip: nls.localize('arduino/sketch/new', 'New'),
priority: 3,
});
}
async newSketch(): Promise<void> {
try {
const sketch = await this.sketchService.createNewSketch();

View File

@@ -0,0 +1,32 @@
import { CommandRegistry } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution, Command } from './contribution';
@injectable()
export class OpenBoardsConfig extends Contribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsConfigDialog)
private readonly boardsConfigDialog: BoardsConfigDialog;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
return (this.boardsServiceProvider.boardsConfig = boardsConfig);
}
},
});
}
}
export namespace OpenBoardsConfig {
export namespace Commands {
export const OPEN_DIALOG: Command = {
id: 'arduino-open-boards-dialog',
};
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import {
Disposable,
@@ -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 { ExampleRef } from '../../common/protocol';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@@ -35,18 +36,19 @@ export class OpenRecentSketch extends SketchContribution {
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void {
const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches);
this.mainMenuManager.update();
};
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
refreshMenu(sketches)
override onStart(): void {
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
this.refreshMenu(sketches)
);
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.sketchService
.recentlyOpenedSketches()
.then((sketches) => this.refreshMenu(sketches));
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
nls.localize('arduino/sketch/openRecent', 'Open Recent'),
@@ -54,21 +56,30 @@ export class OpenRecentSketch extends SketchContribution {
);
}
protected register(sketches: Sketch[]): void {
private refreshMenu(sketches: (Sketch | ExampleRef)[]): void {
this.register(sketches);
this.mainMenuManager.update();
}
protected register(sketches: (Sketch | ExampleRef)[]): void {
const order = 0;
for (const sketch of sketches) {
const { uri } = sketch;
const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` };
const handler = {
execute: () =>
execute: async () => {
const toOpen = Sketch.is(sketch)
? sketch
: await this.sketchService.cloneExample(sketch.sourceUri);
this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
),
toOpen
);
},
};
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(
@@ -80,7 +91,7 @@ export class OpenRecentSketch extends SketchContribution {
}
);
this.toDisposeBeforeRegister.set(
sketch.uri,
uri,
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -13,13 +13,13 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
@@ -27,7 +27,7 @@ export class OpenSketchExternal extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K',

View File

@@ -0,0 +1,108 @@
import { nls } from '@theia/core/lib/common/nls';
import { 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 {
Command,
CommandRegistry,
SketchContribution,
URI,
} from './contribution';
import { SaveAsSketch } from './save-as-sketch';
@injectable()
export class OpenSketchFiles extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI) => this.openSketchFiles(uri),
});
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
execute: (
uri: string,
forceOpen?: boolean,
options?: EditorOpenerOptions
) => {
this.ensureOpened(uri, forceOpen, options);
},
});
}
private async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(
'arduino/common/oldFormat',
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
sketch.name
);
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
this.messageService.info(message, Later, yes).then((answer) => {
if (answer === yes) {
this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
}
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.openFallbackSketch();
} else {
console.error(err);
const message =
err instanceof Error
? err.message
: typeof err === 'string'
? err
: String(err);
this.messageService.error(message);
}
}
}
private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
}
private async ensureOpened(
uri: string,
forceOpen = false,
options?: EditorOpenerOptions
): Promise<unknown> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(
new URI(uri),
options ?? {
mode: 'reveal',
preview: false,
counter: 0,
}
);
}
}
}
export namespace OpenSketchFiles {
export namespace Commands {
export const OPEN_SKETCH_FILES: Command = {
id: 'arduino-open-sketch-files',
};
export const ENSURE_OPENED: Command = {
id: 'arduino-ensure-opened',
};
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
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';
@@ -16,7 +16,6 @@ import {
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples';
@@ -43,7 +42,7 @@ export class OpenSketch extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
@@ -116,7 +115,7 @@ export class OpenSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
@@ -124,22 +123,13 @@ export class OpenSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: nls.localize('vscode/dialogMainService/open', 'Open'),
priority: 4,
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX } from '@theia/core/lib/common/os';
import {
@@ -13,7 +13,7 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(),
@@ -21,7 +21,7 @@ export class QuitApp extends Contribution {
}
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
@@ -32,7 +32,7 @@ export class QuitApp extends Contribution {
}
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) {
registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id,

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -11,16 +11,28 @@ import {
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/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 {
registerCommands(registry: CommandRegistry): void {
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
@@ -28,7 +40,7 @@ export class SaveAsSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
@@ -43,10 +55,11 @@ export class SaveAsSketch extends SketchContribution {
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@@ -63,15 +76,11 @@ export class SaveAsSketch extends SketchContribution {
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = exists
? sketchDirUri.resolve(
sketchDirUri
.resolve(
`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
)
.toString()
)
: sketchDirUri.resolve(sketch.name);
const defaultUri = sketchDirUri.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(
@@ -90,22 +99,63 @@ export class SaveAsSketch extends SketchContribution {
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
if (workspaceUri) {
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
if (markAsRecentlyOpened) {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
});
}
this.workspaceService.open(new URI(workspaceUri), options);
}
return !!workspaceUri;
}
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, object>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
const uriString = uri?.toString();
let relativePath: string;
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (mainFileUri === uriString) {
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketchUri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
}));
}
}
export namespace SaveAsSketch {
@@ -121,12 +171,14 @@ export namespace SaveAsSketch {
* Ignored if `openAfterMove` is `false`.
*/
readonly wipeOriginal?: boolean;
readonly markAsRecentlyOpened?: boolean;
}
export namespace Options {
export const DEFAULT: Options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
markAsRecentlyOpened: false,
};
}
}

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
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';
@@ -9,13 +9,13 @@ import {
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
@@ -27,7 +27,7 @@ export class SaveSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'),
@@ -35,25 +35,16 @@ export class SaveSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
tooltip: nls.localize('vscode/fileCommands/save', 'Save'),
priority: 5,
});
}
async saveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);

View File

@@ -0,0 +1,54 @@
import {
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser/status-bar/status-bar';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { BoardsConfig } from '../boards/boards-config';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution } from './contribution';
@injectable()
export class SelectedBoard extends Contribution {
@inject(StatusBar)
private readonly statusBar: StatusBar;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
override onStart(): void {
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
this.update(config)
);
}
override onReady(): void {
this.update(this.boardsServiceProvider.boardsConfig);
}
private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
? `$(microchip) ${selectedBoard.name}`
: `$(close) ${nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
)}`,
className: 'arduino-selected-board',
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
)
: nls.localize('arduino/common/notConnected', '[not connected]'),
className: 'arduino-selected-port',
});
}
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -18,7 +18,7 @@ export class Settings extends SketchContribution {
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
@@ -39,7 +39,7 @@ export class Settings extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id,
label:
@@ -49,10 +49,13 @@ export class Settings extends SketchContribution {
) + '...',
order: '0',
});
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
registry.registerSubmenu(
ArduinoMenus.FILE__ADVANCED_SUBMENU,
nls.localize('arduino/menu/advanced', 'Advanced')
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
@@ -19,7 +19,10 @@ import {
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@@ -35,7 +38,7 @@ export class SketchControl extends SketchContribution {
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@@ -46,7 +49,7 @@ export class SketchControl extends SketchContribution {
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
{
@@ -55,7 +58,7 @@ export class SketchControl extends SketchContribution {
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
@@ -70,25 +73,22 @@ export class SketchControl extends SketchContribution {
return;
}
const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri);
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch =
await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentsketch = await this.sketchService.getSketchFolder(
parentsketchUri || ''
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
this.allowRename(parentsketch.uri)
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowRename(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -122,10 +122,10 @@ export class SketchControl extends SketchContribution {
}
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
this.allowDelete(parentsketch.uri)
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowDelete(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -200,7 +200,7 @@ export class SketchControl extends SketchContribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
@@ -228,7 +228,7 @@ export class SketchControl extends SketchContribution {
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
@@ -243,7 +243,7 @@ export class SketchControl extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
@@ -276,7 +276,7 @@ export namespace SketchControl {
export namespace Commands {
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
id: 'arduino-open-sketch-control--toolbar',
iconClass: 'fa fa-caret-down',
iconClass: 'fa fa-arduino-sketch-tabs-menu',
};
}
}

View File

@@ -0,0 +1,65 @@
import { SaveableWidget } from '@theia/core/lib/browser/saveable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
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 } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@injectable()
export class SketchFilesTracker extends SketchContribution {
@inject(FileSystemFrontendContribution)
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
private readonly toDisposeOnStop = new DisposableCollection();
override onStart(): void {
this.fileSystemFrontendContribution.onDidChangeEditorFile(
({ type, editor }) => {
if (type === FileChangeType.DELETED) {
const editorWidget = editor;
if (SaveableWidget.is(editorWidget)) {
editorWidget.closeWithoutSaving();
} else {
editorWidget.close();
}
}
}
);
}
override onReady(): void {
this.sketchServiceClient.currentSketch().then(async (sketch) => {
if (CurrentSketch.isValid(sketch)) {
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
if (
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.commandService.executeCommand(
OpenSketchFiles.Commands.ENSURE_OPENED.id,
resource.toString(),
true,
{
mode: 'open',
}
);
}
}
}
})
);
}
});
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
}

View File

@@ -1,21 +1,25 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandRegistry, 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 } from '../../common/protocol';
import {
SketchContainer,
SketchesError,
SketchRef,
} from '../../common/protocol';
import { OpenSketch } from './open-sketch';
import { nls } from '@theia/core/lib/common';
@injectable()
export class Sketchbook extends Examples {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
protected override readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
protected override readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@@ -23,20 +27,22 @@ export class Sketchbook extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
onStart(): void {
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
}
override async onReady(): Promise<void> {
this.update();
}
private update() {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
nls.localize('arduino/sketch/sketchbook', 'Sketchbook'),
@@ -53,14 +59,27 @@ export class Sketchbook extends Examples {
);
}
protected createHandler(uri: string): CommandHandler {
protected override createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
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
);
}
},
};
}

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -39,7 +39,7 @@ export class UploadCertificate extends Contribution {
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
@@ -93,7 +93,7 @@ export class UploadCertificate extends Contribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadCertificate.Commands.OPEN.id,
label: UploadCertificate.Commands.OPEN.label,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -16,7 +16,7 @@ export class UploadFirmware extends Contribution {
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadFirmware.Commands.OPEN, {
execute: async () => {
try {
@@ -30,7 +30,7 @@ export class UploadFirmware extends Contribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadFirmware.Commands.OPEN.id,
label: UploadFirmware.Commands.OPEN.label,

View File

@@ -1,64 +1,49 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService } from '../../common/protocol';
import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
@injectable()
export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
private readonly menuRegistry: MenuModelRegistry;
@inject(UserFieldsDialog)
protected readonly userFieldsDialog: UserFieldsDialog;
private readonly userFieldsDialog: UserFieldsDialog;
protected cachedUserFields: Map<string, BoardUserField[]> = new Map();
private boardRequiresUserFields = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
protected uploadInProgress = false;
protected boardRequiresUserFields = false;
protected readonly menuActionsDisposables = new DisposableCollection();
@postConstruct()
protected init(): void {
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields();
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceClientImpl;
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
@@ -72,17 +57,18 @@ export class UploadSketch extends SketchContribution {
return fqbn + '|' + address;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
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.boardsServiceClientImpl.selectedBoardUserFields()
await this.boardsServiceProvider.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
@@ -104,8 +90,7 @@ export class UploadSketch extends SketchContribution {
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.boardsServiceClientImpl.selectedBoardUserFields())
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
@@ -134,9 +119,8 @@ export class UploadSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
@@ -159,7 +143,7 @@ export class UploadSketch extends SketchContribution {
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
@@ -177,7 +161,7 @@ export class UploadSketch extends SketchContribution {
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
@@ -188,7 +172,7 @@ export class UploadSketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
@@ -199,41 +183,44 @@ export class UploadSketch extends SketchContribution {
}
async uploadSketch(usingProgrammer = false): Promise<void> {
// even with buttons disabled, better to double check if an upload is already in progress
if (this.uploadInProgress) {
return;
}
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
this.sourceOverride(),
]);
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire();
this.clearVisibleNotification();
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort;
const userFields =
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
if (userFields.length === 0 && this.boardRequiresUserFields) {
const verifyOptions =
await this.commandService.executeCommand<CoreService.Options.Compile>(
'arduino-verify-sketch',
<VerifySketchParams>{
exportBinaries: false,
silent: true,
}
);
if (!verifyOptions) {
return;
}
const uploadOptions = await this.uploadOptions(
usingProgrammer,
verifyOptions
);
if (!uploadOptions) {
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',
@@ -243,56 +230,93 @@ export class UploadSketch extends SketchContribution {
return;
}
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
userFields,
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
userFields,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: (progressId, coreService) =>
coreService.upload({ ...uploadOptions, progressId }),
keepOutput: true,
});
this.messageService.info(
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = '';
if (typeof e === 'string') {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire();
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
}
}
private async uploadOptions(
usingProgrammer: boolean,
verifyOptions: CoreService.Options.Compile
): Promise<CoreService.Options.Upload | undefined> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const userFields = this.userFields();
const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
return {
sketch,
fqbn,
...(usingProgrammer && { programmer }),
port,
verbose,
verify,
userFields,
};
}
/**
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
*
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
*/
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
if (port) {
const key = Port.keyOf(port);
for (const candidate of this.boardsServiceProvider.availablePorts) {
if (key === Port.keyOf(candidate) && candidate.properties) {
return {
...port,
properties: deepClone(candidate.properties),
};
}
}
}
return port;
}
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.
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
}
}
export namespace UploadSketch {
@@ -300,7 +324,7 @@ export namespace UploadSketch {
export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch',
};
export const UPLOAD_WITH_CONFIGURATION: Command = {
export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = {
id: 'arduino-upload-with-configuration-sketch',
label: nls.localize(
'arduino/sketch/configureAndUpload',

View File

@@ -1,12 +1,9 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -14,30 +11,37 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler';
export interface VerifySketchParams {
/**
* Same as `CoreService.Options.Compile#exportBinaries`
*/
readonly exportBinaries?: boolean;
/**
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
*/
readonly silent?: boolean;
}
@injectable()
export class VerifySketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
export class VerifySketch extends CoreServiceContribution {
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private verifyInProgress = false;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected verifyInProgress = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(),
execute: (params?: VerifySketchParams) => this.verifySketch(params),
isEnabled: () => !this.verifyInProgress,
});
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true),
execute: () => this.verifySketch({ exportBinaries: true }),
isEnabled: () => !this.verifyInProgress,
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
@@ -50,7 +54,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
@@ -66,7 +70,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
@@ -77,7 +81,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
@@ -87,58 +91,88 @@ export class VerifySketch extends SketchContribution {
});
}
async verifySketch(exportBinaries?: boolean): Promise<void> {
// even with buttons disabled, better to double check if a verify is already in progress
protected override handleError(error: unknown): void {
this.coreErrorHandler.tryHandle(error);
super.handleError(error);
}
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
if (this.verifyInProgress) {
return;
return undefined;
}
// toggle the toolbar button and menu item state.
// verifyInProgress will be set to false whether the compilation fails or not
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
if (!params?.silent) {
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
}
this.clearVisibleNotification();
this.coreErrorHandler.reset();
const options = await this.options(params?.exportBinaries);
if (!options) {
return undefined;
}
await this.doWithProgress({
progressText: nls.localize(
'arduino/sketch/compile',
'Compiling sketch...'
),
this.sourceOverride(),
]);
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
task: (progressId, coreService) =>
coreService.compile({
...options,
progressId,
}),
});
this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 }
);
// Returns with the used options for the compilation
// so that follow-up tasks (such as upload) can reuse the compiled code.
// Note that the `fqbn` is already decorated with the board settings, if any.
return options;
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
return undefined;
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();
if (!params?.silent) {
this.onDidChangeEmitter.fire();
}
}
}
private async options(
exportBinaries?: boolean
): Promise<CoreService.Options.Compile | undefined> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.sourceOverride(),
this.commandService.executeCommand<boolean>(
'arduino-is-optimize-for-debug'
),
]);
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
return {
sketch,
fqbn,
optimizeForDebug: Boolean(optimizeForDebug),
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
};
}
}
export namespace VerifySketch {

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
@@ -117,11 +117,11 @@ export class CreateApi {
headers,
})
).sketches;
if (partialSketches.length != 0) {
if (partialSketches.length !== 0) {
result.sketches = result.sketches.concat(partialSketches);
}
currentOffset = currentOffset + limit;
} while (partialSketches.length != 0);
} while (partialSketches.length !== 0);
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;
@@ -507,7 +507,8 @@ export class CreateApi {
}
private domain(apiVersion = 'v2'): string {
const endpoint = this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint'];
const endpoint =
this.arduinoPreferences['arduino.cloud.sketchSyncEndpoint'];
return `${endpoint}/${apiVersion}`;
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event';
import {

View File

@@ -1,128 +0,0 @@
{
"tokenColors": [
{
"settings": {
"foreground": "#434f54"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#95a5a6cc"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#00979D"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c",
"variable.other"
],
"settings": {
"foreground": "#D35400"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c",
"variable",
"variable.name"
],
"settings": {
"foreground": "#434f54"
}
},
{
"name": "constants",
"scope": [
"string.quoted.double",
"constant"
],
"settings": {
"foreground": "#005C5F"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#728E00"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
],
"colors": {
"list.highlightForeground": "#005c5f",
"list.activeSelectionForeground": "#424242",
"list.activeSelectionBackground": "#DAE3E3",
"list.inactiveSelectionForeground": "#424242",
"list.inactiveSelectionBackground": "#DAE3E3",
"list.hoverBackground": "#ECF1F1",
"progressBar.background": "#005c5f",
"editor.background": "#ffffff",
"editorCursor.foreground": "#434f54",
"editor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00",
"editorWidget.background": "#F7F9F9",
"focusBorder": "#7fcbcd99",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#f7f9f9",
"button.background": "#7fcbcd",
"titleBar.activeBackground": "#005c5f",
"titleBar.activeForeground": "#ffffff",
"terminal.background": "#000000",
"terminal.foreground": "#e0e0e0",
"dropdown.border": "#ececec",
"dropdown.background": "#ececec",
"activityBar.background": "#ececec",
"activityBar.foreground": "#616161",
"statusBar.background": "#005c5f",
"secondaryButton.background": "#b5c8c9",
"secondaryButton.foreground": "#ececec",
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#00979d",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#FFFFFF",
"arduino.output.background": "#000000"
},
"type": "light",
"name": "Arduino"
}

View File

@@ -0,0 +1,166 @@
{
"name": "Arduino dark",
"type": "dark",
"colors": {
"list.highlightForeground": "#0ca1a6",
"list.activeSelectionForeground": "#dae3e3",
"list.activeSelectionBackground": "#0ca1a64d",
"list.inactiveSelectionForeground": "#dae3e3",
"list.inactiveSelectionBackground": "#434f54",
"list.hoverBackground": "#1f272a",
"list.activeSelectionIconForeground": "#0ca1a6",
"progressBar.background": "#005c5f",
"editor.background": "#1f272a",
"editor.foreground": "#dae3e3",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#00818480",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#171e21",
"editorWidget.foreground": "#dae3e3",
"focusBorder": "#dae3e3",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#171e21",
"button.background": "#0ca1a6",
"button.foreground": "#101618",
"button.hoverBackground": "#7fcbcd",
"titleBar.activeBackground": "#171e21",
"titleBar.activeForeground": "#dae3e3",
"terminal.background": "#000000",
"terminal.foreground": "#ffffff",
"dropdown.border": "#7fcbcd",
"dropdown.background": "#2c353a",
"dropdown.foreground": "#dae3e3",
"activityBar.background": "#171e21",
"activityBar.foreground": "#dae3e3",
"activityBar.inactiveForeground": "#4e5b61",
"activityBar.activeBorder": "#0ca1a6",
"statusBar.background": "#171e21",
"secondaryButton.background": "#ff000000",
"secondaryButton.foreground": "#dae3e3",
"secondaryButton.hoverBackground": "#ffffff1a",
"arduino.branding.primary": "#0ca1a6",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#ffffff",
"arduino.output.background": "#000000",
"arduino.toolbar.button.hoverBackground": "#dae3e3",
"arduino.toolbar.button.secondary.label": "#dae3e3",
"arduino.toolbar.button.secondary.hoverBackground": "#dae3e366",
"arduino.toolbar.button.background": "#0ca1a6",
"arduino.toolbar.dropdown.border": "#7fcbcd",
"arduino.toolbar.dropdown.borderActive": "#0ca1a6",
"arduino.toolbar.dropdown.background": "#2c353a",
"arduino.toolbar.dropdown.label": "#dae3e3",
"arduino.toolbar.dropdown.iconSelected": "#3fae98",
"arduino.toolbar.dropdown.option.backgroundHover": "#374146",
"arduino.toolbar.dropdown.option.backgroundSelected": "#4e5b61",
"arduino.toolbar.toggleBackground": "#f1c40f",
"sideBar.background": "#101618",
"sideBar.foreground": "#dae3e3",
"input.background": "#000000",
"foreground": "#dae3e3",
"settings.headerForeground": "#dae3e3",
"tree.indentGuidesStroke": "#374146",
"tab.unfocusedActiveForeground": "#dae3e3",
"tab.inactiveBackground": "#171e21",
"textLink.foreground": "#0ca1a6"
},
"tokenColors": [
{
"name": "",
"settings": {
"foreground": "#dae3e3"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#7f8c8d"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#0ca1a6"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c",
"variable.other"
],
"settings": {
"foreground": "#F39C12"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"variable",
"variable.name"
],
"settings": {
"foreground": "#dae3e3"
}
},
{
"name": "function preprocessor",
"scope": [
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"foreground": "#569CD6"
}
},
{
"name": "constants",
"scope": [
"string.quoted.double",
"string.quoted.other.lt-gt",
"constant"
],
"settings": {
"foreground": "#7fcbcd"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#C586C0"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
]
}

View File

@@ -0,0 +1,166 @@
{
"name": "Arduino default",
"type": "default",
"colors": {
"list.highlightForeground": "#008184",
"list.activeSelectionForeground": "#4e5b61",
"list.activeSelectionBackground": "#00818433",
"list.inactiveSelectionForeground": "#4e5b61",
"list.inactiveSelectionBackground": "#dae3e3",
"list.hoverBackground": "#ecf1f1",
"list.activeSelectionIconForeground": "#008184",
"progressBar.background": "#005c5f",
"editor.background": "#ffffff",
"editor.foreground": "#4e5b61",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#7fcbcdb3",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#f7f9f9",
"editorWidget.foreground": "#4e5b61",
"focusBorder": "#7fcbcd",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#ecf1f1",
"button.background": "#008184",
"button.foreground": "#f7f9f9",
"button.hoverBackground": "#005C5F",
"titleBar.activeBackground": "#006d70",
"titleBar.activeForeground": "#f7f9f9",
"terminal.background": "#000000",
"terminal.foreground": "#ffffff",
"dropdown.border": "#dae3e3",
"dropdown.background": "#ffffff",
"dropdown.foreground": "#4e5b61",
"activityBar.background": "#ecf1f1",
"activityBar.foreground": "#4e5b61",
"activityBar.inactiveForeground": "#bdc7c7",
"activityBar.activeBorder": "#008184",
"statusBar.background": "#006d70",
"secondaryButton.background": "#ff000000",
"secondaryButton.foreground": "#008184",
"secondaryButton.hoverBackground": "#005c5f1a",
"arduino.branding.primary": "#008184",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#ffffff",
"arduino.output.background": "#000000",
"arduino.toolbar.button.hoverBackground": "#f7f9f9",
"arduino.toolbar.button.secondary.label": "#dae3e3",
"arduino.toolbar.button.secondary.hoverBackground": "#dae3e366",
"arduino.toolbar.button.background": "#7fcbcd",
"arduino.toolbar.dropdown.border": "#dae3e3",
"arduino.toolbar.dropdown.borderActive": "#7fcbcd",
"arduino.toolbar.dropdown.background": "#ffffff",
"arduino.toolbar.dropdown.label": "#4e5b61",
"arduino.toolbar.dropdown.iconSelected": "#1da086",
"arduino.toolbar.dropdown.option.backgroundHover": "#ecf1f1",
"arduino.toolbar.dropdown.option.backgroundSelected": "#dae3e3",
"arduino.toolbar.toggleBackground": "#f1c40f",
"sideBar.background": "#f7f9f9",
"sideBar.foreground": "#4e5b61",
"input.background": "#ffffff",
"foreground": "#4e5b61",
"settings.headerForeground": "#4e5b61",
"tree.indentGuidesStroke": "#dae3e3",
"tab.unfocusedActiveForeground": "#4e5b61",
"tab.inactiveBackground": "#ecf1f1",
"textLink.foreground": "#008184"
},
"tokenColors": [
{
"name": "",
"settings": {
"foreground": "#434f54"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#95a5a6cc"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#00979D"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c",
"variable.other"
],
"settings": {
"foreground": "#D35400"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"variable",
"variable.name"
],
"settings": {
"foreground": "#434f54"
}
},
{
"name": "function preprocessor",
"scope": [
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"foreground": "#9e846d"
}
},
{
"name": "constants",
"scope": [
"string.quoted.double",
"string.quoted.other.lt-gt",
"constant"
],
"settings": {
"foreground": "#005C5F"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#728E00"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
]
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
export const CertificateAddComponent = ({
addCertificate,
@@ -8,9 +8,12 @@ export const CertificateAddComponent = ({
}): React.ReactElement => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[]
);
return (
<form

View File

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

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
@@ -94,7 +94,7 @@ export const CertificateUploaderComponent = ({
>
<button
type="button"
className="theia-button primary add-cert-btn"
className="theia-button secondary add-cert-btn"
onClick={() => {
showAdd ? setShowAdd(false) : setShowAdd(true);
}}

View File

@@ -1,9 +1,13 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from '@theia/core/shared/react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
@@ -19,6 +23,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@@ -37,6 +42,9 @@ export class UploadCertificateDialogWidget extends ReactWidget {
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
@@ -66,10 +74,12 @@ export class UploadCertificateDialogWidget extends ReactWidget {
}
});
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.appStateService.reachedState('ready').then(() =>
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
})
);
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
@@ -139,7 +149,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
constructor(
@inject(UploadCertificateDialogProps)
protected readonly props: UploadCertificateDialogProps
protected override readonly props: UploadCertificateDialogProps
) {
super({
title: nls.localize(
@@ -147,6 +157,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
'Upload SSL Root Certificates'
),
});
this.node.id = 'certificate-uploader-dialog-container';
this.contentNode.classList.add('certificate-uploader-dialog');
this.acceptButton = undefined;
}
@@ -155,7 +166,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -165,21 +176,21 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
override close(): void {
if (this.busy) {
return;
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from 'electron';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
@@ -149,7 +149,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
protected override readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
@@ -159,7 +159,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -168,12 +168,12 @@ export class ShareSketchDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
@@ -19,7 +19,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
constructor(
@inject(DoNotAskAgainDialogProps)
protected readonly props: DoNotAskAgainDialogProps
protected override readonly props: DoNotAskAgainDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
@@ -42,7 +42,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
@@ -65,7 +65,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
}
}
protected setErrorMessage(error: DialogError): void {
protected override setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}

View File

@@ -1,5 +1,6 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Port } from '../../../common/protocol';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
@@ -20,7 +21,7 @@ export const FirmwareUploaderComponent = ({
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
@@ -81,7 +82,7 @@ export const FirmwareUploaderComponent = ({
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {

View File

@@ -1,9 +1,13 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from '@theia/core/shared/react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
@@ -15,6 +19,8 @@ import {
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@@ -24,6 +30,9 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
@@ -38,7 +47,8 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
@postConstruct()
protected init(): void {
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.appStatusService.reachedState('ready').then(async () => {
const fqbns = await this.arduinoFirmwareUploader.updatableBoards();
this.updatableFqbns = fqbns;
this.update();
});
@@ -49,14 +59,14 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
});
}
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
onCloseRequest(msg: Message): void {
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
@@ -88,9 +98,10 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
constructor(
@inject(UploadFirmwareDialogProps)
protected readonly props: UploadFirmwareDialogProps
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
@@ -99,7 +110,7 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -109,21 +120,21 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
override close(): void {
if (this.busy) {
return;
}

View File

@@ -1,42 +1,37 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';
export type IDEUpdaterComponentProps = {
updateInfo: UpdateInfo;
windowService: WindowService;
export interface UpdateProgress {
progressInfo?: ProgressInfo | undefined;
downloadFinished?: boolean;
downloadStarted?: boolean;
progress?: ProgressInfo;
error?: Error;
onDownload: () => void;
onClose: () => void;
onSkipVersion: () => void;
onCloseAndInstall: () => void;
};
}
export interface IDEUpdaterComponentProps {
updateInfo: UpdateInfo;
updateProgress: UpdateProgress;
}
export const IDEUpdaterComponent = ({
updateInfo: { version, releaseNotes },
downloadStarted = false,
downloadFinished = false,
windowService,
progress,
error,
onDownload,
onClose,
onSkipVersion,
onCloseAndInstall,
updateInfo,
updateProgress: {
downloadStarted = false,
downloadFinished = false,
progressInfo,
error,
},
}: IDEUpdaterComponentProps): React.ReactElement => {
const changelogDivRef = React.useRef() as React.MutableRefObject<
HTMLDivElement
>;
const { version, releaseNotes } = updateInfo;
const changelogDivRef =
React.useRef() as React.MutableRefObject<HTMLDivElement>;
React.useEffect(() => {
if (!!releaseNotes) {
if (!!releaseNotes && changelogDivRef.current) {
let changelog: string;
if (typeof releaseNotes === 'string') changelog = releaseNotes;
else
@@ -58,12 +53,7 @@ export const IDEUpdaterComponent = ({
changelogDivRef.current
);
}
}, [releaseNotes]);
const closeButton = (
<button onClick={onClose} type="button" className="theia-button secondary">
{nls.localize('arduino/ide-updater/notNowButton', 'Not now')}
</button>
);
}, [updateInfo]);
const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded">
@@ -80,19 +70,6 @@ export const IDEUpdaterComponent = ({
'Close the software and install the update on your machine.'
)}
</div>
<div className="buttons-container">
{closeButton}
<button
onClick={onCloseAndInstall}
type="button"
className="theia-button close-and-install"
>
{nls.localize(
'arduino/ide-updater/closeAndInstallButton',
'Close and Install'
)}
</button>
</div>
</div>
);
@@ -104,7 +81,7 @@ export const IDEUpdaterComponent = ({
'Downloading the latest version of the Arduino IDE.'
)}
</div>
<ProgressBar percent={progress?.percent} showPercentage />
<ProgressBar percent={progressInfo?.percent} showPercentage />
</div>
);
@@ -130,46 +107,14 @@ export const IDEUpdaterComponent = ({
)}
</div>
{releaseNotes && (
<div className="dialogRow">
<div className="changelog-container" ref={changelogDivRef} />
<div className="dialogRow changelog-container">
<div className="changelog" ref={changelogDivRef} />
</div>
)}
<div className="buttons-container">
<button
onClick={onSkipVersion}
type="button"
className="theia-button secondary skip-version"
>
{nls.localize(
'arduino/ide-updater/skipVersionButton',
'Skip Version'
)}
</button>
<div className="push"></div>
{closeButton}
<button
onClick={onDownload}
type="button"
className="theia-button primary"
>
{nls.localize('arduino/ide-updater/downloadButton', 'Download')}
</button>
</div>
</div>
</div>
);
const onGoToDownloadClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
event.nativeEvent.preventDefault();
windowService.openNewWindow(target.href, { external: true });
onClose();
}
};
const GoToDownloadPage: () => React.ReactElement = () => (
<div className="ide-updater-dialog--go-to-download-page">
<div>
@@ -178,19 +123,6 @@ export const IDEUpdaterComponent = ({
"An update for the Arduino IDE is available, but we're not able to download and install it automatically. Please go to the download page and download the latest version from there."
)}
</div>
<div className="buttons-container">
{closeButton}
<a
className="theia-button primary"
href="https://www.arduino.cc/en/software#experimental-software"
onClick={onGoToDownloadClick}
>
{nls.localize(
'arduino/ide-updater/goToDownloadButton',
'Go To Download'
)}
</a>
</div>
</div>
);

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