Compare commits

...

58 Commits

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

Closes #964
Closes #1484

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

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

* Removed unused file

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

* yarn i18n:generate for the settings

* Updated the desription for markdown description.

* Lintered the code

* Remove undesirable whitespaces

* Applied kittaakos suggestions

* Removed extra whitespaces

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

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

* consider exceptions and fix styling

* fix onBlur with empty strings

* always set the internal state value

* misc fixes

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

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

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

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

Closes #573

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

* use arduio-cli sorting fix

* re-use code to handle board list response

* change `handleListBoards` visibility to `private`

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

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

Closes #1526

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

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

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

* debounce interface scale updates

* limit font-size + refactor

* remove excessive settings duplicate

* remove useless async

* fix interface scale step

* change mainMenuManager visibility to private

* fix menu registration

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

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

Closes #1525

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

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

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

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

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

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

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

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

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

* fix "Configure and Upload" label

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

The localization process follows the following steps:

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

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

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

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

Arduino IDE also uses localized strings from several other sources:

- VS Code language packs
- Arduino CLI

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1431

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

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

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

* use same dialog for new file and rename

* fix board list getting small when filtering

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

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

* move user fields logic into own contribution

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

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-19 12:17:13 +02:00
138 changed files with 7191 additions and 2664 deletions

View File

@@ -9,7 +9,7 @@ contact_links:
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
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
about: Learn about submitting issue reports to this repository.
- name: Contributor guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide

View File

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

View File

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

View File

@@ -53,8 +53,6 @@ import {
DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import {
@@ -207,12 +205,8 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
import {
MonacoEditorFactory,
@@ -337,6 +331,10 @@ import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
import { UserFields } from './contributions/user-fields';
import { UpdateIndexes } from './contributions/update-indexes';
import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
@@ -401,6 +399,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
LibraryListWidgetFrontendContribution
);
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
// Sketch list service
bind(SketchesService)
@@ -467,6 +466,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
BoardsListWidgetFrontendContribution
);
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -604,9 +604,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoEditorProvider).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).toService(EditorManager);
@@ -616,17 +613,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(SearchInWorkspaceFactory)
.inSingletonScope();
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
({ container }) => {
const childContainer = createTreeContainer(container);
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
childContainer
.rebind(TreeWidget)
.toService(SearchInWorkspaceResultTreeWidget);
return childContainer.get(SearchInWorkspaceResultTreeWidget);
}
);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
rebind(TheiaApplicationConnectionStatusContribution).toService(
@@ -761,7 +747,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch);
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
AttachedBoardsChangeEvent,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
@@ -113,11 +112,14 @@ export class BoardsConfig extends React.Component<
);
}
}),
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
this.updatePorts(
event.newState.ports,
AttachedBoardsChangeEvent.diff(event).detached.ports
)
this.props.boardsServiceProvider.onAvailablePortsChanged(
({ newState, oldState }) => {
const removedPorts = oldState.filter(
(oldPort) =>
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
);
this.updatePorts(newState, removedPorts);
}
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
@@ -132,7 +134,7 @@ export class BoardsConfig extends React.Component<
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexDidUpdate(() =>
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
@@ -259,9 +261,12 @@ export class BoardsConfig extends React.Component<
override render(): React.ReactNode {
return (
<>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
@@ -299,6 +304,18 @@ export class BoardsConfig extends React.Component<
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
@@ -315,19 +332,17 @@ export class BoardsConfig extends React.Component<
/>
<i className="fa fa-search"></i>
</div>
<div className="boards list">
{Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
))}
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
@@ -342,7 +357,7 @@ export class BoardsConfig extends React.Component<
);
}
return !ports.length ? (
<div className="loading noselect">
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
@@ -374,7 +389,9 @@ export class BoardsConfig extends React.Component<
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);

View File

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

View File

@@ -63,7 +63,10 @@ export class BoardsServiceProvider
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
newState: Port[];
oldState: Port[];
}>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/**
@@ -120,8 +123,12 @@ export class BoardsServiceProvider
const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state);
this._attachedBoards = attachedBoards;
const oldState = this._availablePorts.slice();
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
await this.reconcileAvailableBoards();
@@ -229,8 +236,12 @@ export class BoardsServiceProvider
}
this._attachedBoards = event.newState.boards;
const oldState = this._availablePorts.slice();
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an
@@ -398,14 +409,16 @@ export class BoardsServiceProvider
}
async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
if (!this._boardsConfig.selectedBoard) {
return [];
}
const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) {
return [];
}
const protocol = this._boardsConfig.selectedPort.protocol;
// Protocol must be set to `default` when uploading without a port selected:
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
}
@@ -600,7 +613,7 @@ export class BoardsServiceProvider
boardsConfig.selectedBoard &&
availableBoards.every(({ selected }) => !selected)
) {
let port = boardsConfig.selectedPort
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.
@@ -611,7 +624,7 @@ export class BoardsServiceProvider
// 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
port = availableBoards[found].port;
availableBoards.splice(found, 1);
}
availableBoards.push({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
/* Tools */ registry.registerCommand(
EditContributions.Commands.AUTO_FORMAT,
{ execute: () => this.run('editor.action.formatDocument') }
@@ -147,23 +123,6 @@ ${value}
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: nls.localize('vscode/findController/startFindAction', 'Find'),
@@ -220,15 +179,6 @@ ${value}
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
@@ -315,12 +265,6 @@ export namespace EditContributions {
export const USE_FOR_FIND: Command = {
id: 'arduino-for-find',
};
export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size',
};
export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size',
};
export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
SketchContribution,
URI,
@@ -17,11 +16,6 @@ export class NewSketch extends SketchContribution {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(),
});
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
});
}
override registerMenus(registry: MenuModelRegistry): void {
@@ -54,8 +48,5 @@ export namespace NewSketch {
export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch',
};
export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar',
};
}
}

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
private async saveAs(
{
execOnlyIfTemp,
openAfterMove,
@@ -58,7 +58,10 @@ export class SaveAsSketch extends SketchContribution {
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@@ -68,27 +71,38 @@ export class SaveAsSketch extends SketchContribution {
return false;
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = new URI(configuration.sketchDirUri);
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch.
const containerDirUri = isTemp
? sketchbookDirUri
: !sketchbookDirUri.isEqualOrParent(sketchUri)
? sketchbookDirUri
: sketchUri.parent;
const exists = await this.fileService.exists(
containerDirUri.resolve(sketch.name)
);
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI(
(await this.configService.getConfiguration()).sketchDirUri
);
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = sketchDirUri.resolve(
const defaultUri = containerDirUri.resolve(
exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name
);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath || canceled) {
return false;
}

View File

@@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
@@ -19,12 +18,6 @@ export class SaveSketch extends SketchContribution {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () =>
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
});
}
override registerMenus(registry: MenuModelRegistry): void {
@@ -68,8 +61,5 @@ export namespace SaveSketch {
export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch',
};
export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar',
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,53 @@ import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
class HistoryList {
private readonly items: string[] = [];
private index = -1;
constructor(private readonly size = 100) {}
push(val: string): void {
if (val !== this.items[this.items.length - 1]) {
this.items.push(val);
}
while (this.items.length > this.size) {
this.items.shift();
}
this.index = -1;
}
previous(): string {
if (this.index === -1) {
this.index = this.items.length - 1;
return this.items[this.index];
}
if (this.hasPrevious) {
return this.items[--this.index];
}
return this.items[this.index];
}
private get hasPrevious(): boolean {
return this.index >= 1;
}
next(): string {
if (this.index === this.items.length - 1) {
this.index = -1;
return '';
}
if (this.hasNext) {
return this.items[++this.index];
}
return '';
}
private get hasNext(): boolean {
return this.index >= 0 && this.index !== this.items.length - 1;
}
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
@@ -16,6 +63,7 @@ export namespace SerialMonitorSendInput {
export interface State {
text: string;
connected: boolean;
history: HistoryList;
}
}
@@ -27,7 +75,7 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: true };
this.state = { text: '', connected: true, history: new HistoryList() };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
@@ -90,7 +138,7 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected setRef = (element: HTMLElement | null) => {
protected setRef = (element: HTMLElement | null): void => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
@@ -110,7 +158,17 @@ export class SerialMonitorSendInput extends React.Component<
if (keyCode) {
const { key } = keyCode;
if (key === Key.ENTER) {
const { text } = this.state;
this.onSend();
if (text) {
this.state.history.push(text);
}
} else if (key === Key.ARROW_UP) {
this.setState({ text: this.state.history.previous() });
} else if (key === Key.ARROW_DOWN) {
this.setState({ text: this.state.history.next() });
} else if (key === Key.ESCAPE) {
this.setState({ text: '' });
}
}
}

View File

@@ -18,6 +18,7 @@ import {
CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW,
} from '../../../common/ipc-communication';
import { nls } from '@theia/core/lib/common/nls';
const queryString = require('query-string');
@@ -107,7 +108,12 @@ export class PlotterFrontendContribution extends Contribution {
if (wsPort) {
this.open(wsPort);
} else {
this.messageService.error(`Couldn't open serial plotter`);
this.messageService.error(
nls.localize(
'arduino/contributions/plotter/couldNotOpen',
"Couldn't open serial plotter"
)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import {
import {
SketchesService,
Sketch,
SketchesError,
} from '../../../common/protocol/sketches-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import {
@@ -38,6 +39,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
private readonly providers: ContributionProvider<StartupTaskProvider>;
private version?: string;
private _workspaceError: Error | undefined;
async onStart(application: FrontendApplication): Promise<void> {
const info = await this.applicationServer.getApplicationInfo();
@@ -51,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
this.onCurrentWidgetChange({ newValue, oldValue: null });
}
get workspaceError(): Error | undefined {
return this._workspaceError;
}
protected override async toFileStat(
uri: string | URI | undefined
): Promise<FileStat | undefined> {
@@ -59,6 +65,31 @@ export class WorkspaceService extends TheiaWorkspaceService {
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
}
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
// Nothing will work if the workspace file is invalid. Users tend to start (see #964) IDE2 from the `.ino` files,
// so here, IDE2 tries to load the sketch via the CLI from the main sketch file URI.
// If loading the sketch is OK, IDE2 starts and uses the sketch folder as the workspace root instead of the sketch file.
// If loading fails due to invalid name error, IDE2 loads a temp sketch and preserves the startup error, and offers the sketch move to the user later.
// If loading the sketch fails, create a fallback sketch and open the new temp sketch folder as the workspace root.
if (stat.isFile && stat.resource.path.ext === '.ino') {
try {
const sketch = await this.sketchService.loadSketch(
stat.resource.toString()
);
return this.toFileStat(sketch.uri);
} catch (err) {
if (SketchesError.InvalidName.is(err)) {
this._workspaceError = err;
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
} else if (SketchesError.NotFound.is(err)) {
this._workspaceError = err;
const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri);
}
throw err;
}
}
return stat;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
import { TheiaElectronWindow } from './theia/theia-electron-window';
import { ElectronNativeKeymap } from '@theia/core/lib/electron-main/electron-native-keymap';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
@@ -58,4 +59,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
// https://github.com/eclipse-theia/theia/issues/11688
bind(ElectronNativeKeymap).toSelf().inSingletonScope();
bind(ElectronMainApplicationContribution).toService(ElectronNativeKeymap);
});

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import {
ArduinoFirmwareUploader,
ArduinoFirmwareUploaderPath,
} from '../common/protocol/arduino-firmware-uploader';
import { ILogger } from '@theia/core/lib/common/logger';
import {
BackendApplicationContribution,
@@ -26,7 +25,7 @@ import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connec
import { CoreClientProvider } from './core-client-provider';
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { SketchesServiceImpl } from './sketches-service-impl';
import {
SketchesService,
@@ -40,7 +39,6 @@ import {
ArduinoDaemon,
ArduinoDaemonPath,
} from '../common/protocol/arduino-daemon';
import { ConfigServiceImpl } from './config-service-impl';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
@@ -332,6 +330,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
'fwuploader', // Arduino Firmware uploader
'discovery-log', // Boards discovery
'config', // Logger for the CLI config reading and manipulation
'sketches-service', // For creating, loading, and cloning sketches
MonitorManagerName, // Logger for the monitor manager and its services
MonitorServiceName,
].forEach((name) => bindChildLogger(bind, name));

View File

@@ -6,7 +6,7 @@ import { deepClone } from '@theia/core/lib/common/objects';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BackendApplicationContribution } from '@theia/core/lib/node';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { Disposable } from '@theia/core/lib/common/disposable';
import { v4 } from 'uuid';
import { Unknown } from '../common/nls';
import {
@@ -78,14 +78,6 @@ export class BoardDiscovery
onStart(): void {
this.start();
this.onClientDidRefresh(() => this.restart());
}
private async restart(): Promise<void> {
this.logger.info('restarting before stop');
await this.stop();
this.logger.info('restarting after stop');
return this.start();
}
onStop(): void {

View File

@@ -32,6 +32,8 @@ import { CoreClientAware } from './core-client-provider';
import {
BoardDetailsRequest,
BoardDetailsResponse,
BoardListAllRequest,
BoardListAllResponse,
BoardSearchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import {
@@ -199,8 +201,28 @@ export class BoardsServiceImpl
const req = new BoardSearchRequest();
req.setSearchArgs(query || '');
req.setInstance(instance);
return this.handleListBoards(client.boardSearch.bind(client), req);
}
async getInstalledBoards(): Promise<BoardWithPackage[]> {
const { instance, client } = await this.coreClient;
const req = new BoardListAllRequest();
req.setInstance(instance);
return this.handleListBoards(client.boardListAll.bind(client), req);
}
private async handleListBoards(
getBoards: (
request: BoardListAllRequest | BoardSearchRequest,
callback: (
error: ServiceError | null,
response: BoardListAllResponse
) => void
) => void,
request: BoardListAllRequest | BoardSearchRequest
): Promise<BoardWithPackage[]> {
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
client.boardSearch(req, (error, resp) => {
getBoards(request, (error, resp) => {
if (error) {
reject(error);
return;

View File

@@ -220,6 +220,9 @@ export class UpdateIndexRequest extends jspb.Message {
getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined;
setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): UpdateIndexRequest;
getIgnoreCustomPackageIndexes(): boolean;
setIgnoreCustomPackageIndexes(value: boolean): UpdateIndexRequest;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateIndexRequest.AsObject;
@@ -234,6 +237,7 @@ export class UpdateIndexRequest extends jspb.Message {
export namespace UpdateIndexRequest {
export type AsObject = {
instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject,
ignoreCustomPackageIndexes: boolean,
}
}

View File

@@ -1811,7 +1811,8 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.toObject = functio
*/
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.toObject = function(includeInstance, msg) {
var f, obj = {
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f)
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f),
ignoreCustomPackageIndexes: jspb.Message.getBooleanFieldWithDefault(msg, 2, false)
};
if (includeInstance) {
@@ -1853,6 +1854,10 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.deserializeBinaryFromReader
reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader);
msg.setInstance(value);
break;
case 2:
var value = /** @type {boolean} */ (reader.readBool());
msg.setIgnoreCustomPackageIndexes(value);
break;
default:
reader.skipField();
break;
@@ -1890,6 +1895,13 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.serializeBinaryToWriter = fu
cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter
);
}
f = message.getIgnoreCustomPackageIndexes();
if (f) {
writer.writeBool(
2,
f
);
}
};
@@ -1930,6 +1942,24 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.hasInstance = func
};
/**
* optional bool ignore_custom_package_indexes = 2;
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.getIgnoreCustomPackageIndexes = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false));
};
/**
* @param {boolean} value
* @return {!proto.cc.arduino.cli.commands.v1.UpdateIndexRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.setIgnoreCustomPackageIndexes = function(value) {
return jspb.Message.setProto3BooleanField(this, 2, value);
};

View File

@@ -28,21 +28,26 @@ export namespace Instance {
}
export class DownloadProgress extends jspb.Message {
getUrl(): string;
setUrl(value: string): DownloadProgress;
getFile(): string;
setFile(value: string): DownloadProgress;
hasStart(): boolean;
clearStart(): void;
getStart(): DownloadProgressStart | undefined;
setStart(value?: DownloadProgressStart): DownloadProgress;
getTotalSize(): number;
setTotalSize(value: number): DownloadProgress;
getDownloaded(): number;
setDownloaded(value: number): DownloadProgress;
hasUpdate(): boolean;
clearUpdate(): void;
getUpdate(): DownloadProgressUpdate | undefined;
setUpdate(value?: DownloadProgressUpdate): DownloadProgress;
getCompleted(): boolean;
setCompleted(value: boolean): DownloadProgress;
hasEnd(): boolean;
clearEnd(): void;
getEnd(): DownloadProgressEnd | undefined;
setEnd(value?: DownloadProgressEnd): DownloadProgress;
getMessageCase(): DownloadProgress.MessageCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DownloadProgress.AsObject;
@@ -55,12 +60,97 @@ export class DownloadProgress extends jspb.Message {
}
export namespace DownloadProgress {
export type AsObject = {
start?: DownloadProgressStart.AsObject,
update?: DownloadProgressUpdate.AsObject,
end?: DownloadProgressEnd.AsObject,
}
export enum MessageCase {
MESSAGE_NOT_SET = 0,
START = 1,
UPDATE = 2,
END = 3,
}
}
export class DownloadProgressStart extends jspb.Message {
getUrl(): string;
setUrl(value: string): DownloadProgressStart;
getLabel(): string;
setLabel(value: string): DownloadProgressStart;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DownloadProgressStart.AsObject;
static toObject(includeInstance: boolean, msg: DownloadProgressStart): DownloadProgressStart.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: DownloadProgressStart, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): DownloadProgressStart;
static deserializeBinaryFromReader(message: DownloadProgressStart, reader: jspb.BinaryReader): DownloadProgressStart;
}
export namespace DownloadProgressStart {
export type AsObject = {
url: string,
file: string,
totalSize: number,
label: string,
}
}
export class DownloadProgressUpdate extends jspb.Message {
getDownloaded(): number;
setDownloaded(value: number): DownloadProgressUpdate;
getTotalSize(): number;
setTotalSize(value: number): DownloadProgressUpdate;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DownloadProgressUpdate.AsObject;
static toObject(includeInstance: boolean, msg: DownloadProgressUpdate): DownloadProgressUpdate.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: DownloadProgressUpdate, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): DownloadProgressUpdate;
static deserializeBinaryFromReader(message: DownloadProgressUpdate, reader: jspb.BinaryReader): DownloadProgressUpdate;
}
export namespace DownloadProgressUpdate {
export type AsObject = {
downloaded: number,
completed: boolean,
totalSize: number,
}
}
export class DownloadProgressEnd extends jspb.Message {
getSuccess(): boolean;
setSuccess(value: boolean): DownloadProgressEnd;
getMessage(): string;
setMessage(value: string): DownloadProgressEnd;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DownloadProgressEnd.AsObject;
static toObject(includeInstance: boolean, msg: DownloadProgressEnd): DownloadProgressEnd.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: DownloadProgressEnd, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): DownloadProgressEnd;
static deserializeBinaryFromReader(message: DownloadProgressEnd, reader: jspb.BinaryReader): DownloadProgressEnd;
}
export namespace DownloadProgressEnd {
export type AsObject = {
success: boolean,
message: string,
}
}

View File

@@ -17,6 +17,10 @@ var global = Function('return this')();
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Board', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressEnd', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressStart', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InstalledPlatformReference', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Instance', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Platform', null, global);
@@ -55,7 +59,7 @@ if (goog.DEBUG && !COMPILED) {
* @constructor
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
jspb.Message.initialize(this, opt_data, 0, -1, null, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgress, jspb.Message);
if (goog.DEBUG && !COMPILED) {
@@ -65,6 +69,69 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgress';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressStart, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressStart';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressEnd';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -322,6 +389,33 @@ proto.cc.arduino.cli.commands.v1.Instance.prototype.setId = function(value) {
/**
* Oneof group definitions for this message. Each group defines the field
* numbers belonging to that group. When of these fields' value is set, all
* other fields in the group are cleared. During deserialization, if multiple
* fields are encountered for a group, only the last value seen will be kept.
* @private {!Array<!Array<number>>}
* @const
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_ = [[1,2,3]];
/**
* @enum {number}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase = {
MESSAGE_NOT_SET: 0,
START: 1,
UPDATE: 2,
END: 3
};
/**
* @return {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getMessageCase = function() {
return /** @type {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase} */(jspb.Message.computeOneofCase(this, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0]));
};
if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -353,11 +447,9 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.toObject = function(
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.toObject = function(includeInstance, msg) {
var f, obj = {
url: jspb.Message.getFieldWithDefault(msg, 1, ""),
file: jspb.Message.getFieldWithDefault(msg, 2, ""),
totalSize: jspb.Message.getFieldWithDefault(msg, 3, 0),
downloaded: jspb.Message.getFieldWithDefault(msg, 4, 0),
completed: jspb.Message.getBooleanFieldWithDefault(msg, 5, false)
start: (f = msg.getStart()) && proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(includeInstance, f),
update: (f = msg.getUpdate()) && proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(includeInstance, f),
end: (f = msg.getEnd()) && proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(includeInstance, f)
};
if (includeInstance) {
@@ -395,24 +487,19 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.deserializeBinaryFromReader =
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setUrl(value);
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart;
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader);
msg.setStart(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setFile(value);
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate;
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader);
msg.setUpdate(value);
break;
case 3:
var value = /** @type {number} */ (reader.readInt64());
msg.setTotalSize(value);
break;
case 4:
var value = /** @type {number} */ (reader.readInt64());
msg.setDownloaded(value);
break;
case 5:
var value = /** @type {boolean} */ (reader.readBool());
msg.setCompleted(value);
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd;
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader);
msg.setEnd(value);
break;
default:
reader.skipField();
@@ -442,6 +529,251 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.serializeBinary = fu
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getStart();
if (f != null) {
writer.writeMessage(
1,
f,
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter
);
}
f = message.getUpdate();
if (f != null) {
writer.writeMessage(
2,
f,
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter
);
}
f = message.getEnd();
if (f != null) {
writer.writeMessage(
3,
f,
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter
);
}
};
/**
* optional DownloadProgressStart start = 1;
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getStart = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressStart} */ (
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressStart, 1));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setStart = function(value) {
return jspb.Message.setOneofWrapperField(this, 1, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearStart = function() {
return this.setStart(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasStart = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional DownloadProgressUpdate update = 2;
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUpdate = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} */ (
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, 2));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUpdate = function(value) {
return jspb.Message.setOneofWrapperField(this, 2, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearUpdate = function() {
return this.setUpdate(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasUpdate = function() {
return jspb.Message.getField(this, 2) != null;
};
/**
* optional DownloadProgressEnd end = 3;
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getEnd = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} */ (
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, 3));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setEnd = function(value) {
return jspb.Message.setOneofWrapperField(this, 3, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearEnd = function() {
return this.setEnd(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasEnd = function() {
return jspb.Message.getField(this, 3) != null;
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject = function(includeInstance, msg) {
var f, obj = {
url: jspb.Message.getFieldWithDefault(msg, 1, ""),
label: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart;
return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setUrl(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setLabel(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getUrl();
if (f.length > 0) {
@@ -450,34 +782,13 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = func
f
);
}
f = message.getFile();
f = message.getLabel();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getTotalSize();
if (f !== 0) {
writer.writeInt64(
3,
f
);
}
f = message.getDownloaded();
if (f !== 0) {
writer.writeInt64(
4,
f
);
}
f = message.getCompleted();
if (f) {
writer.writeBool(
5,
f
);
}
};
@@ -485,89 +796,355 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = func
* optional string url = 1;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUrl = function() {
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getUrl = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUrl = function(value) {
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setUrl = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
/**
* optional string file = 2;
* optional string label = 2;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getFile = function() {
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getLabel = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setFile = function(value) {
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setLabel = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* optional int64 total_size = 3;
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject = function(includeInstance, msg) {
var f, obj = {
downloaded: jspb.Message.getFieldWithDefault(msg, 1, 0),
totalSize: jspb.Message.getFieldWithDefault(msg, 2, 0)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate;
return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {number} */ (reader.readInt64());
msg.setDownloaded(value);
break;
case 2:
var value = /** @type {number} */ (reader.readInt64());
msg.setTotalSize(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getDownloaded();
if (f !== 0) {
writer.writeInt64(
1,
f
);
}
f = message.getTotalSize();
if (f !== 0) {
writer.writeInt64(
2,
f
);
}
};
/**
* optional int64 downloaded = 1;
* @return {number}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getTotalSize = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getDownloaded = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
};
/**
* @param {number} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setTotalSize = function(value) {
return jspb.Message.setProto3IntField(this, 3, value);
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setDownloaded = function(value) {
return jspb.Message.setProto3IntField(this, 1, value);
};
/**
* optional int64 downloaded = 4;
* optional int64 total_size = 2;
* @return {number}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getDownloaded = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getTotalSize = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));
};
/**
* @param {number} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setDownloaded = function(value) {
return jspb.Message.setProto3IntField(this, 4, value);
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setTotalSize = function(value) {
return jspb.Message.setProto3IntField(this, 2, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(opt_includeInstance, this);
};
/**
* optional bool completed = 5;
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject = function(includeInstance, msg) {
var f, obj = {
success: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
message: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd;
return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {boolean} */ (reader.readBool());
msg.setSuccess(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setMessage(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getSuccess();
if (f) {
writer.writeBool(
1,
f
);
}
f = message.getMessage();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
};
/**
* optional bool success = 1;
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getCompleted = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 5, false));
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getSuccess = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false));
};
/**
* @param {boolean} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setCompleted = function(value) {
return jspb.Message.setProto3BooleanField(this, 5, value);
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setSuccess = function(value) {
return jspb.Message.setProto3BooleanField(this, 1, value);
};
/**
* optional string message = 2;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getMessage = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this
*/
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setMessage = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};

View File

@@ -765,9 +765,6 @@ export class Library extends jspb.Message {
getContainerPlatform(): string;
setContainerPlatform(value: string): Library;
getRealName(): string;
setRealName(value: string): Library;
getDotALinkage(): boolean;
setDotALinkage(value: boolean): Library;
@@ -836,7 +833,6 @@ export namespace Library {
sourceDir: string,
utilityDir: string,
containerPlatform: string,
realName: string,
dotALinkage: boolean,
precompiled: boolean,
ldFlags: string,

View File

@@ -5447,7 +5447,6 @@ proto.cc.arduino.cli.commands.v1.Library.toObject = function(includeInstance, ms
sourceDir: jspb.Message.getFieldWithDefault(msg, 11, ""),
utilityDir: jspb.Message.getFieldWithDefault(msg, 12, ""),
containerPlatform: jspb.Message.getFieldWithDefault(msg, 14, ""),
realName: jspb.Message.getFieldWithDefault(msg, 16, ""),
dotALinkage: jspb.Message.getBooleanFieldWithDefault(msg, 17, false),
precompiled: jspb.Message.getBooleanFieldWithDefault(msg, 18, false),
ldFlags: jspb.Message.getFieldWithDefault(msg, 19, ""),
@@ -5548,10 +5547,6 @@ proto.cc.arduino.cli.commands.v1.Library.deserializeBinaryFromReader = function(
var value = /** @type {string} */ (reader.readString());
msg.setContainerPlatform(value);
break;
case 16:
var value = /** @type {string} */ (reader.readString());
msg.setRealName(value);
break;
case 17:
var value = /** @type {boolean} */ (reader.readBool());
msg.setDotALinkage(value);
@@ -5724,13 +5719,6 @@ proto.cc.arduino.cli.commands.v1.Library.serializeBinaryToWriter = function(mess
f
);
}
f = message.getRealName();
if (f.length > 0) {
writer.writeString(
16,
f
);
}
f = message.getDotALinkage();
if (f) {
writer.writeBool(
@@ -6084,24 +6072,6 @@ proto.cc.arduino.cli.commands.v1.Library.prototype.setContainerPlatform = functi
};
/**
* optional string real_name = 16;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.Library.prototype.getRealName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 16, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.Library} returns this
*/
proto.cc.arduino.cli.commands.v1.Library.prototype.setRealName = function(value) {
return jspb.Message.setProto3StringField(this, 16, value);
};
/**
* optional bool dot_a_linkage = 17;
* @return {boolean}

View File

@@ -26,7 +26,6 @@ import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { deepClone } from '@theia/core';
import { duration } from '../common/decorators';
const deepmerge = require('deepmerge');
@@ -56,7 +55,10 @@ export class ConfigServiceImpl
this.loadCliConfig().then(async (cliConfig) => {
this.cliConfig = cliConfig;
if (this.cliConfig) {
const config = await this.mapCliConfigToAppConfig(this.cliConfig);
const [config] = await Promise.all([
this.mapCliConfigToAppConfig(this.cliConfig),
this.ensureUserDirExists(this.cliConfig),
]);
if (config) {
this.config = config;
this.ready.resolve();
@@ -126,7 +128,6 @@ export class ConfigServiceImpl
return this.daemon.getVersion();
}
@duration()
protected async loadCliConfig(
initializeIfAbsent = true
): Promise<DefaultCliConfig | undefined> {
@@ -263,4 +264,11 @@ export class ConfigServiceImpl
grpc.credentials.createInsecure()
) as SettingsServiceClient;
}
// #1445
private async ensureUserDirExists(
cliConfig: DefaultCliConfig
): Promise<void> {
await fs.mkdir(cliConfig.directories.user, { recursive: true });
}
}

View File

@@ -5,7 +5,7 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { Emitter } from '@theia/core/lib/common/event';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import {
@@ -19,8 +19,15 @@ import {
UpdateLibrariesIndexResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { NotificationServiceServer } from '../common/protocol';
import { Deferred, retry } from '@theia/core/lib/common/promise-util';
import {
IndexType,
IndexUpdateDidCompleteParams,
IndexUpdateSummary,
IndexUpdateDidFailParams,
IndexUpdateWillStartParams,
NotificationServiceServer,
} from '../common/protocol';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
Status as RpcStatus,
Status,
@@ -32,6 +39,7 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import {
IndexesUpdateProgressHandler,
ExecuteWithProgress,
DownloadResult,
} from './grpc-progressible';
import type { DefaultCliConfig } from './cli-config';
import { ServiceError } from './service-error';
@@ -45,16 +53,19 @@ export class CoreClientProvider {
@inject(NotificationServiceServer)
private readonly notificationService: NotificationServiceServer;
private ready = new Deferred<void>();
private pending: Deferred<CoreClientProvider.Client> | undefined;
private _client: CoreClientProvider.Client | undefined;
private readonly toDisposeBeforeCreate = new DisposableCollection();
/**
* See `CoreService#indexUpdateSummaryBeforeInit`.
*/
private readonly beforeInitSummary = {} as IndexUpdateSummary;
private readonly toDisposeOnCloseClient = new DisposableCollection();
private readonly toDisposeAfterDidCreate = new DisposableCollection();
private readonly onClientReadyEmitter =
new Emitter<CoreClientProvider.Client>();
private readonly onClientReady = this.onClientReadyEmitter.event;
private readonly onClientDidRefreshEmitter =
new Emitter<CoreClientProvider.Client>();
private ready = new Deferred<void>();
private pending: Deferred<CoreClientProvider.Client> | undefined;
private _client: CoreClientProvider.Client | undefined;
@postConstruct()
protected init(): void {
@@ -65,7 +76,9 @@ export class CoreClientProvider {
});
this.daemon.onDaemonStarted((port) => this.create(port));
this.daemon.onDaemonStopped(() => this.closeClient());
this.configService.onConfigChange(() => this.refreshIndexes());
this.configService.onConfigChange(
() => this.client.then((client) => this.updateIndex(client, ['platform'])) // Assuming 3rd party URL changes. No library index update is required.
);
}
get tryGetClient(): CoreClientProvider.Client | undefined {
@@ -80,7 +93,7 @@ export class CoreClientProvider {
if (!this.pending) {
this.pending = new Deferred();
this.toDisposeAfterDidCreate.pushAll([
Disposable.create(() => (this.pending = undefined)),
Disposable.create(() => (this.pending = undefined)), // TODO: reject all pending requests before unsetting the ref?
this.onClientReady((client) => {
this.pending?.resolve(client);
this.toDisposeAfterDidCreate.dispose();
@@ -90,8 +103,9 @@ export class CoreClientProvider {
return this.pending.promise;
}
get onClientDidRefresh(): Event<CoreClientProvider.Client> {
return this.onClientDidRefreshEmitter.event;
async refresh(): Promise<void> {
const client = await this.client;
await this.initInstance(client);
}
/**
@@ -101,7 +115,7 @@ export class CoreClientProvider {
this.closeClient();
const address = this.address(port);
const client = await this.createClient(address);
this.toDisposeBeforeCreate.pushAll([
this.toDisposeOnCloseClient.pushAll([
Disposable.create(() => client.client.close()),
Disposable.create(() => {
this.ready.reject(
@@ -113,7 +127,6 @@ export class CoreClientProvider {
}),
]);
await this.initInstanceWithFallback(client);
setTimeout(async () => this.refreshIndexes(), 10_000); // Update the indexes asynchronously
return this.useClient(client);
}
@@ -136,12 +149,17 @@ export class CoreClientProvider {
try {
await this.initInstance(client);
} catch (err) {
if (err instanceof IndexUpdateRequiredBeforeInitError) {
if (err instanceof MustUpdateIndexesBeforeInitError) {
console.error(
'The primary packages indexes are missing. Running indexes update before initializing the core gRPC client',
err.message
);
await this.updateIndexes(client); // TODO: this should run without the 3rd party URLs
await this.updateIndex(client, Array.from(err.indexTypesToUpdate));
const updatedAt = new Date().toISOString();
// Clients will ask for it after they connect.
err.indexTypesToUpdate.forEach(
(type) => (this.beforeInitSummary[type] = updatedAt)
);
await this.initInstance(client);
console.info(
`Downloaded the primary package indexes, and successfully initialized the core gRPC client.`
@@ -165,7 +183,7 @@ export class CoreClientProvider {
}
private closeClient(): void {
return this.toDisposeBeforeCreate.dispose();
return this.toDisposeOnCloseClient.dispose();
}
private async createClient(
@@ -248,45 +266,66 @@ export class CoreClientProvider {
}
/**
* Updates all indexes and runs an init to [reload the indexes](https://github.com/arduino/arduino-cli/pull/1274#issue-866154638).
* `update3rdPartyPlatforms` has not effect if `types` is `['library']`.
*/
private async refreshIndexes(): Promise<void> {
const client = this._client;
if (client) {
const progressHandler = this.createProgressHandler();
try {
await this.updateIndexes(client, progressHandler);
async updateIndex(
client: CoreClientProvider.Client,
types: IndexType[]
): Promise<void> {
let error: unknown | undefined = undefined;
const progressHandler = this.createProgressHandler(types);
try {
const updates: Promise<void>[] = [];
if (types.includes('platform')) {
updates.push(this.updatePlatformIndex(client, progressHandler));
}
if (types.includes('library')) {
updates.push(this.updateLibraryIndex(client, progressHandler));
}
await Promise.all(updates);
} catch (err) {
// This is suboptimal but the core client must be re-initialized even if the index update has failed and the request was rejected.
error = err;
} finally {
// IDE2 reloads the index only and if only at least one download success is available.
if (
progressHandler.results.some(
(result) => !DownloadResult.isError(result)
)
) {
await this.initInstance(client);
// notify clients about the index update only after the client has been "re-initialized" and the new content is available.
progressHandler.reportEnd();
this.onClientDidRefreshEmitter.fire(client);
} catch (err) {
console.error('Failed to update indexes', err);
progressHandler.reportError(
ServiceError.is(err) ? err.details : String(err)
);
}
if (error) {
console.error(`Failed to update ${types.join(', ')} indexes.`, error);
const downloadErrors = progressHandler.results
.filter(DownloadResult.isError)
.map(({ url, message }) => `${message}: ${url}`)
.join(' ');
const message = ServiceError.is(error)
? `${error.details}${downloadErrors ? ` ${downloadErrors}` : ''}`
: String(error);
// IDE2 keeps only the most recent error message. Previous errors might have been fixed with the fallback initialization.
this.beforeInitSummary.message = message;
// Toast the error message, so tha the user has chance to fix it if it was a client error (HTTP 4xx).
progressHandler.reportError(message);
}
}
}
private async updateIndexes(
client: CoreClientProvider.Client,
progressHandler?: IndexesUpdateProgressHandler
): Promise<void> {
await Promise.all([
this.updateIndex(client, progressHandler),
this.updateLibraryIndex(client, progressHandler),
]);
get indexUpdateSummaryBeforeInit(): IndexUpdateSummary {
return { ...this.beforeInitSummary };
}
private async updateIndex(
private async updatePlatformIndex(
client: CoreClientProvider.Client,
progressHandler?: IndexesUpdateProgressHandler
): Promise<void> {
return this.doUpdateIndex(
() =>
client.client.updateIndex(
new UpdateIndexRequest().setInstance(client.instance)
new UpdateIndexRequest().setInstance(client.instance) // Always updates both the primary and the 3rd party package indexes.
),
progressHandler,
'platform-index'
@@ -318,50 +357,45 @@ export class CoreClientProvider {
task?: string
): Promise<void> {
const progressId = progressHandler?.progressId;
return retry(
() =>
new Promise<void>((resolve, reject) => {
responseProvider()
.on(
'data',
ExecuteWithProgress.createDataCallback({
responseService: {
appendToOutput: ({ chunk: message }) => {
console.log(
`core-client-provider${task ? ` [${task}]` : ''}`,
message
);
progressHandler?.reportProgress(message);
},
},
progressId,
})
)
.on('error', reject)
.on('end', resolve);
}),
50,
3
);
return new Promise<void>((resolve, reject) => {
responseProvider()
.on(
'data',
ExecuteWithProgress.createDataCallback({
responseService: {
appendToOutput: ({ chunk: message }) => {
console.log(
`core-client-provider${task ? ` [${task}]` : ''}`,
message
);
progressHandler?.reportProgress(message);
},
},
reportResult: (result) => progressHandler?.reportResult(result),
progressId,
})
)
.on('error', reject)
.on('end', resolve);
});
}
private createProgressHandler(): IndexesUpdateProgressHandler {
private createProgressHandler(
types: IndexType[]
): IndexesUpdateProgressHandler {
const additionalUrlsCount =
this.configService.cliConfiguration?.board_manager?.additional_urls
?.length ?? 0;
return new IndexesUpdateProgressHandler(
additionalUrlsCount,
(progressMessage) =>
return new IndexesUpdateProgressHandler(types, additionalUrlsCount, {
onProgress: (progressMessage) =>
this.notificationService.notifyIndexUpdateDidProgress(progressMessage),
({ progressId, message }) =>
this.notificationService.notifyIndexUpdateDidFail({
progressId,
message,
}),
(progressId) =>
this.notificationService.notifyIndexWillUpdate(progressId),
(progressId) => this.notificationService.notifyIndexDidUpdate(progressId)
);
onError: (params: IndexUpdateDidFailParams) =>
this.notificationService.notifyIndexUpdateDidFail(params),
onStart: (params: IndexUpdateWillStartParams) =>
this.notificationService.notifyIndexUpdateWillStart(params),
onComplete: (params: IndexUpdateDidCompleteParams) =>
this.notificationService.notifyIndexUpdateDidComplete(params),
});
}
private address(port: string): string {
@@ -405,6 +439,7 @@ export namespace CoreClientProvider {
export abstract class CoreClientAware {
@inject(CoreClientProvider)
private readonly coreClientProvider: CoreClientProvider;
/**
* Returns with a promise that resolves when the core client is initialized and ready.
*/
@@ -412,20 +447,38 @@ export abstract class CoreClientAware {
return this.coreClientProvider.client;
}
protected get onClientDidRefresh(): Event<CoreClientProvider.Client> {
return this.coreClientProvider.onClientDidRefresh;
/**
* Updates the index of the given `type` and returns with a promise which resolves when the core gPRC client has been reinitialized.
*/
async updateIndex({ types }: { types: IndexType[] }): Promise<void> {
const client = await this.coreClient;
return this.coreClientProvider.updateIndex(client, types);
}
async indexUpdateSummaryBeforeInit(): Promise<IndexUpdateSummary> {
await this.coreClient;
return this.coreClientProvider.indexUpdateSummaryBeforeInit;
}
refresh(): Promise<void> {
return this.coreClientProvider.refresh();
}
}
class IndexUpdateRequiredBeforeInitError extends Error {
constructor(causes: RpcStatus.AsObject[]) {
class MustUpdateIndexesBeforeInitError extends Error {
readonly indexTypesToUpdate: Set<IndexType>;
constructor(causes: [RpcStatus.AsObject, IndexType][]) {
super(`The index of the cores and libraries must be updated before initializing the core gRPC client.
The following problems were detected during the gRPC client initialization:
${causes
.map(({ code, message }) => ` - code: ${code}, message: ${message}`)
.map(
([{ code, message }, type]) =>
`[${type}-index] - code: ${code}, message: ${message}`
)
.join('\n')}
`);
Object.setPrototypeOf(this, IndexUpdateRequiredBeforeInitError.prototype);
Object.setPrototypeOf(this, MustUpdateIndexesBeforeInitError.prototype);
this.indexTypesToUpdate = new Set(causes.map(([, type]) => type));
if (!causes.length) {
throw new Error(`expected non-empty 'causes'`);
}
@@ -435,41 +488,66 @@ ${causes
function isIndexUpdateRequiredBeforeInit(
status: RpcStatus[],
cliConfig: DefaultCliConfig
): IndexUpdateRequiredBeforeInitError | undefined {
const causes = status
.filter((s) =>
IndexUpdateRequiredBeforeInit.map((predicate) =>
predicate(s, cliConfig)
).some(Boolean)
)
.map((s) => RpcStatus.toObject(false, s));
): MustUpdateIndexesBeforeInitError | undefined {
const causes = status.reduce((acc, curr) => {
for (const [predicate, type] of IndexUpdateRequiredPredicates) {
if (predicate(curr, cliConfig)) {
acc.push([curr.toObject(false), type]);
return acc;
}
}
return acc;
}, [] as [RpcStatus.AsObject, IndexType][]);
return causes.length
? new IndexUpdateRequiredBeforeInitError(causes)
? new MustUpdateIndexesBeforeInitError(causes)
: undefined;
}
const IndexUpdateRequiredBeforeInit = [
isPackageIndexMissingStatus,
isDiscoveryNotFoundStatus,
interface Predicate {
(
status: RpcStatus,
{
directories: { data },
}: DefaultCliConfig
): boolean;
}
const IndexUpdateRequiredPredicates: [Predicate, IndexType][] = [
[isPrimaryPackageIndexMissingStatus, 'platform'],
[isDiscoveryNotFoundStatus, 'platform'],
[isLibraryIndexMissingStatus, 'library'],
];
function isPackageIndexMissingStatus(
// Loading index file: loading json index file /path/to/package_index.json: open /path/to/package_index.json: no such file or directory
function isPrimaryPackageIndexMissingStatus(
status: RpcStatus,
{ directories: { data } }: DefaultCliConfig
): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('loading json index file') &&
(message.includes(join(data, 'package_index.json')) ||
message.includes(join(data, 'library_index.json')));
message.includes(join(data, 'package_index.json'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return evaluate(status, predicate);
}
// Error loading hardware platform: discovery $TOOL_NAME not found
function isDiscoveryNotFoundStatus(status: RpcStatus): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('discovery') &&
(message.includes('not found') || message.includes('not installed'));
(message.includes('not found') ||
message.includes('loading hardware platform'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
return evaluate(status, predicate);
}
// Loading index file: reading library_index.json: open /path/to/library_index.json: no such file or directory
function isLibraryIndexMissingStatus(
status: RpcStatus,
{ directories: { data } }: DefaultCliConfig
): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('index file') &&
message.includes('reading') &&
message.includes(join(data, 'library_index.json'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return evaluate(status, predicate);
}
function evaluate(
subject: RpcStatus,
predicate: (error: RpcStatus.AsObject) => boolean

View File

@@ -11,14 +11,10 @@ import {
SketchContainer,
} from '../common/protocol/sketches-service';
import { ExamplesService } from '../common/protocol/examples-service';
import {
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol';
import { duration } from '../common/decorators';
import { LibraryLocation, LibraryPackage } from '../common/protocol';
import { URI } from '@theia/core/lib/common/uri';
import { Path } from '@theia/core/lib/common/path';
import { LibraryServiceImpl } from './library-service-impl';
interface BuiltInSketchRef {
readonly name: string;
@@ -84,8 +80,8 @@ export class BuiltInExamplesServiceImpl {
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(LibraryService)
private readonly libraryService: LibraryService;
@inject(LibraryServiceImpl)
private readonly libraryService: LibraryServiceImpl;
@inject(BuiltInExamplesServiceImpl)
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
@@ -94,7 +90,6 @@ export class ExamplesServiceImpl implements ExamplesService {
return this.builtInExamplesService.builtIns();
}
@duration()
async installed({ fqbn }: { fqbn?: string }): Promise<{
user: SketchContainer[];
current: SketchContainer[];
@@ -129,11 +124,11 @@ export class ExamplesServiceImpl implements ExamplesService {
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
*/
private async tryGroupExamples({
label,
name,
exampleUris,
installDirUri,
}: LibraryPackage): Promise<SketchContainer> {
const container = SketchContainer.create(label);
const container = SketchContainer.create(name);
if (!installDirUri || !exampleUris.length) {
return container;
}

View File

@@ -1,4 +1,11 @@
import { v4 } from 'uuid';
import {
IndexType,
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
IndexUpdateSummary,
IndexUpdateWillStartParams,
} from '../common/protocol';
import {
ProgressMessage,
ResponseService,
@@ -11,6 +18,9 @@ import {
import {
DownloadProgress,
TaskProgress,
DownloadProgressStart,
DownloadProgressUpdate,
DownloadProgressEnd,
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
import {
@@ -81,7 +91,9 @@ namespace IndexProgressResponse {
);
}
export function workUnit(response: IndexProgressResponse): UnitOfWork {
return { download: response.getDownloadProgress() };
return {
download: response.getDownloadProgress(),
};
}
}
/**
@@ -152,26 +164,33 @@ export namespace ExecuteWithProgress {
*/
readonly progressId?: string;
readonly responseService: Partial<ResponseService>;
/**
* It's only relevant for index updates to build a summary of possible client (4xx) and server (5xx) errors when downloading the files during the index update. It's missing for lib/platform installations.
*/
readonly reportResult?: (result: DownloadResult) => void;
}
export function createDataCallback<R extends ProgressResponse>({
responseService,
progressId,
reportResult,
}: ExecuteWithProgress.Options): (response: R) => void {
const uuid = v4();
let localFile = '';
let localTotalSize = Number.NaN;
let message = '';
let url = '';
return (response: R) => {
if (DEBUG) {
const json = toJson(response);
if (json) {
console.log(`Progress response [${uuid}]: ${json}`);
console.debug(`[gRPC progress] Progress response [${uuid}]: ${json}`);
}
}
const unitOfWork = resolve(response);
const { task, download } = unitOfWork;
if (!download && !task) {
// report a fake unknown progress.
// Report a fake unknown progress if progress ID is available.
// When a progress ID is available, a connected client is setting the progress ID.
// Hence, it's listening to progress updates.
if (unitOfWork === UnitOfWork.Unknown && progressId) {
if (progressId) {
responseService.reportProgress?.({
@@ -187,7 +206,7 @@ export namespace ExecuteWithProgress {
// Technically, it does not cause an error, but could mess up the progress reporting.
// See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630.
console.warn(
"Implementation error. Neither 'download' nor 'task' is available."
`Implementation error. None of the following properties were available on the response: 'task', 'download'`
);
}
return;
@@ -219,43 +238,32 @@ export namespace ExecuteWithProgress {
}
}
} else if (download) {
if (download.getFile() && !localFile) {
localFile = download.getFile();
}
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
localTotalSize = download.getTotalSize();
}
// This happens only once per file download.
if (download.getTotalSize() && localFile) {
responseService.appendToOutput?.({ chunk: `${localFile}\n` });
}
if (progressId && localFile) {
let work: ProgressMessage.Work | undefined = undefined;
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
work = {
total: localTotalSize,
done: download.getDownloaded(),
};
}
responseService.reportProgress?.({
progressId,
message: `Downloading ${localFile}`,
work,
});
}
if (download.getCompleted()) {
// Discard local state.
if (progressId && !Number.isNaN(localTotalSize)) {
const phase = phaseOf(download);
if (phase instanceof DownloadProgressStart) {
message = phase.getLabel();
url = phase.getUrl();
responseService.appendToOutput?.({ chunk: `${message}\n` });
} else if (phase instanceof DownloadProgressUpdate) {
if (progressId && message) {
responseService.reportProgress?.({
progressId,
message: '',
work: { done: Number.NaN, total: Number.NaN },
message,
work: {
total: phase.getTotalSize(),
done: phase.getDownloaded(),
},
});
}
localFile = '';
localTotalSize = Number.NaN;
} else if (phase instanceof DownloadProgressEnd) {
if (url && reportResult) {
reportResult({
url,
message: phase.getMessage(),
success: phase.getSuccess(),
});
}
message = '';
url = '';
}
}
};
@@ -274,31 +282,40 @@ export namespace ExecuteWithProgress {
return {};
}
function toJson(response: ProgressResponse): string | undefined {
let object: Record<string, unknown> | undefined = undefined;
if (response instanceof LibraryInstallResponse) {
object = LibraryInstallResponse.toObject(false, response);
} else if (response instanceof LibraryUninstallResponse) {
object = LibraryUninstallResponse.toObject(false, response);
} else if (response instanceof ZipLibraryInstallResponse) {
object = ZipLibraryInstallResponse.toObject(false, response);
} else if (response instanceof PlatformInstallResponse) {
object = PlatformInstallResponse.toObject(false, response);
} else if (response instanceof PlatformUninstallResponse) {
object = PlatformUninstallResponse.toObject(false, response);
} else if (response instanceof UpdateIndexResponse) {
object = UpdateIndexResponse.toObject(false, response);
} else if (response instanceof UpdateLibrariesIndexResponse) {
object = UpdateLibrariesIndexResponse.toObject(false, response);
} else if (response instanceof UpdateCoreLibrariesIndexResponse) {
object = UpdateCoreLibrariesIndexResponse.toObject(false, response);
} else if (response instanceof CompileResponse) {
object = CompileResponse.toObject(false, response);
return JSON.stringify(response.toObject(false));
}
function phaseOf(
download: DownloadProgress
): DownloadProgressStart | DownloadProgressUpdate | DownloadProgressEnd {
let start: undefined | DownloadProgressStart = undefined;
let update: undefined | DownloadProgressUpdate = undefined;
let end: undefined | DownloadProgressEnd = undefined;
if (download.hasStart()) {
start = download.getStart();
} else if (download.hasUpdate()) {
update = download.getUpdate();
} else if (download.hasEnd()) {
end = download.getEnd();
} else {
throw new Error(
`Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify(
download.toObject(false)
)}`
);
}
if (!object) {
console.warn('Unhandled gRPC response', response);
return undefined;
if (start) {
return start;
} else if (update) {
return update;
} else if (end) {
return end;
} else {
throw new Error(
`Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify(
download.toObject(false)
)}`
);
}
return JSON.stringify(object);
}
}
@@ -306,33 +323,39 @@ export class IndexesUpdateProgressHandler {
private done = 0;
private readonly total: number;
readonly progressId: string;
readonly results: DownloadResult[];
constructor(
private types: IndexType[],
additionalUrlsCount: number,
private readonly onProgress: (progressMessage: ProgressMessage) => void,
private readonly onError?: ({
progressId,
message,
}: {
progressId: string;
message: string;
}) => void,
private readonly onStart?: (progressId: string) => void,
private readonly onEnd?: (progressId: string) => void
private readonly options: {
onProgress: (progressMessage: ProgressMessage) => void;
onError?: (params: IndexUpdateDidFailParams) => void;
onStart?: (params: IndexUpdateWillStartParams) => void;
onComplete?: (params: IndexUpdateDidCompleteParams) => void;
}
) {
this.progressId = v4();
this.total = IndexesUpdateProgressHandler.total(additionalUrlsCount);
this.results = [];
this.total = IndexesUpdateProgressHandler.total(types, additionalUrlsCount);
// Note: at this point, the IDE2 backend might not have any connected clients, so this notification is not delivered to anywhere
// Hence, clients must handle gracefully when no `willUpdate` is received before any `didProgress`.
this.onStart?.(this.progressId);
// Hence, clients must handle gracefully when no `willStart` event is received before any `didProgress`.
this.options.onStart?.({ progressId: this.progressId, types });
}
reportEnd(): void {
this.onEnd?.(this.progressId);
const updatedAt = new Date().toISOString();
this.options.onComplete?.({
progressId: this.progressId,
summary: this.types.reduce((summary, type) => {
summary[type] = updatedAt;
return summary;
}, {} as IndexUpdateSummary),
});
}
reportProgress(message: string): void {
this.onProgress({
this.options.onProgress({
message,
progressId: this.progressId,
work: { total: this.total, done: ++this.done },
@@ -340,15 +363,44 @@ export class IndexesUpdateProgressHandler {
}
reportError(message: string): void {
this.onError?.({ progressId: this.progressId, message });
this.options.onError?.({
progressId: this.progressId,
message,
types: this.types,
});
}
private static total(additionalUrlsCount: number): number {
// +1 for the `package_index.tar.bz2` when updating the platform index.
const totalPlatformIndexCount = additionalUrlsCount + 1;
// The `library_index.json.gz` and `library_index.json.sig` when running the library index update.
const totalLibraryIndexCount = 2;
reportResult(result: DownloadResult): void {
this.results.push(result);
}
private static total(
types: IndexType[],
additionalUrlsCount: number
): number {
let total = 0;
if (types.includes('library')) {
// The `library_index.json.gz` and `library_index.json.sig` when running the library index update.
total += 2;
}
if (types.includes('platform')) {
// +1 for the `package_index.tar.bz2` when updating the platform index.
total += additionalUrlsCount + 1;
}
// +1 for the `initInstance` call after the index update (`reportEnd`)
return totalPlatformIndexCount + totalLibraryIndexCount + 1;
return total + 1;
}
}
export interface DownloadResult {
readonly url: string;
readonly success: boolean;
readonly message?: string;
}
export namespace DownloadResult {
export function isError(
arg: DownloadResult
): arg is DownloadResult & { message: string } {
return !!arg.message && !arg.success;
}
}

View File

@@ -3,150 +3,45 @@ import {
LocalizationRegistry,
} from '@theia/core/lib/node/i18n/localization-contribution';
import { injectable } from '@theia/core/shared/inversify';
import { join } from 'path';
@injectable()
export class ArduinoLocalizationContribution
implements LocalizationContribution
{
// 0. index: locale
// 1. index: optional JSON file to `require` (if differs from the locale)
// If you touch the locales, please keep the alphabetical order. Also in the `package.json` for the VS Code language packs. Thank you! ❤️
// Note that IDE2 has more translations than available VS Code language packs. (https://github.com/arduino/arduino-ide/issues/1447)
private readonly locales: ReadonlyArray<[string, string?]> = [
['bg'],
['cs'],
['de'],
['es'],
['fr'],
['hu'],
// ['id'], Does not have Transifex translations, but has a VS Code language pack available on Open VSX.
['it'],
['ja'],
['ko'],
['nl'],
['pl'],
['pt-br', 'pt'],
['ru'],
['tr'],
['uk', 'uk_UA'],
['zh-cn', 'zh'],
];
async registerLocalizations(registry: LocalizationRegistry): Promise<void> {
registry.registerLocalizationFromRequire(
'af',
require('../../../build/i18n/af.json')
);
registry.registerLocalizationFromRequire(
'en',
require('../../../build/i18n/en.json')
);
registry.registerLocalizationFromRequire(
'fr',
require('../../../build/i18n/fr.json')
);
registry.registerLocalizationFromRequire(
'ko',
require('../../../build/i18n/ko.json')
);
registry.registerLocalizationFromRequire(
'pt-br',
require('../../../build/i18n/pt.json')
);
registry.registerLocalizationFromRequire(
'uk_UA',
require('../../../build/i18n/uk_UA.json')
);
registry.registerLocalizationFromRequire(
'ar',
require('../../../build/i18n/ar.json')
);
registry.registerLocalizationFromRequire(
'es',
require('../../../build/i18n/es.json')
);
registry.registerLocalizationFromRequire(
'he',
require('../../../build/i18n/he.json')
);
registry.registerLocalizationFromRequire(
'my_MM',
require('../../../build/i18n/my_MM.json')
);
registry.registerLocalizationFromRequire(
'ro',
require('../../../build/i18n/ro.json')
);
registry.registerLocalizationFromRequire(
'zh-cn',
require('../../../build/i18n/zh.json')
);
registry.registerLocalizationFromRequire(
'bg',
require('../../../build/i18n/bg.json')
);
registry.registerLocalizationFromRequire(
'eu',
require('../../../build/i18n/eu.json')
);
registry.registerLocalizationFromRequire(
'hu',
require('../../../build/i18n/hu.json')
);
registry.registerLocalizationFromRequire(
'ne',
require('../../../build/i18n/ne.json')
);
registry.registerLocalizationFromRequire(
'ru',
require('../../../build/i18n/ru.json')
);
registry.registerLocalizationFromRequire(
'zh_TW',
require('../../../build/i18n/zh_TW.json')
);
registry.registerLocalizationFromRequire(
'de',
require('../../../build/i18n/de.json')
);
registry.registerLocalizationFromRequire(
'fa',
require('../../../build/i18n/fa.json')
);
registry.registerLocalizationFromRequire(
'it',
require('../../../build/i18n/it.json')
);
registry.registerLocalizationFromRequire(
'nl',
require('../../../build/i18n/nl.json')
);
registry.registerLocalizationFromRequire(
'sv_SE',
require('../../../build/i18n/sv_SE.json')
);
registry.registerLocalizationFromRequire(
'el',
require('../../../build/i18n/el.json')
);
registry.registerLocalizationFromRequire(
'fil',
require('../../../build/i18n/fil.json')
);
registry.registerLocalizationFromRequire(
'ja',
require('../../../build/i18n/ja.json')
);
registry.registerLocalizationFromRequire(
'pl',
require('../../../build/i18n/pl.json')
);
registry.registerLocalizationFromRequire(
'tr',
require('../../../build/i18n/tr.json')
);
for (const [locale, jsonFilename] of this.locales) {
registry.registerLocalizationFromRequire(
locale,
require(join(
__dirname,
`../../../build/i18n/${jsonFilename ?? locale}.json`
))
);
}
}
}

View File

@@ -23,8 +23,8 @@ export class LocalizationBackendContribution extends TheiaLocalizationBackendCon
app.get('/i18n/:locale', async (req, res) => {
let locale = req.params.locale;
/*
Waiting for the deploy of the language plugins is neecessary to avoid checking the available
languages before they're finished to be loaded: https://github.com/eclipse-theia/theia/issues/11471
Waiting for the deploy of the language plugins is necessary to avoid checking the available
languages before they're finished to be loaded: https://github.com/eclipse-theia/theia/issues/11471
*/
const start = performance.now();
await this.initialized.promise;

View File

@@ -66,7 +66,7 @@ export class LibraryServiceImpl
if (installedLib.hasLibrary()) {
const lib = installedLib.getLibrary();
if (lib) {
installedLibsIdx.set(lib.getRealName(), installedLib);
installedLibsIdx.set(lib.getName(), installedLib);
}
}
}
@@ -210,7 +210,6 @@ export class LibraryServiceImpl
return toLibrary(
{
name: library.getName(),
label: library.getRealName(),
installedVersion,
installable: true,
description: library.getSentence(),
@@ -443,7 +442,6 @@ function toLibrary(
): LibraryPackage {
return {
name: '',
label: '',
exampleUris: [],
installable: false,
deprecated: false,

View File

@@ -8,6 +8,9 @@ import type {
Config,
Sketch,
ProgressMessage,
IndexUpdateWillStartParams,
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
} from '../common/protocol';
@injectable()
@@ -16,8 +19,8 @@ export class NotificationServiceServerImpl
{
private readonly clients: NotificationServiceClient[] = [];
notifyIndexWillUpdate(progressId: string): void {
this.clients.forEach((client) => client.notifyIndexWillUpdate(progressId));
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.clients.forEach((client) => client.notifyIndexUpdateWillStart(params));
}
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
@@ -26,20 +29,14 @@ export class NotificationServiceServerImpl
);
}
notifyIndexDidUpdate(progressId: string): void {
this.clients.forEach((client) => client.notifyIndexDidUpdate(progressId));
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
this.clients.forEach((client) =>
client.notifyIndexUpdateDidComplete(params)
);
}
notifyIndexUpdateDidFail({
progressId,
message,
}: {
progressId: string;
message: string;
}): void {
this.clients.forEach((client) =>
client.notifyIndexUpdateDidFail({ progressId, message })
);
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
this.clients.forEach((client) => client.notifyIndexUpdateDidFail(params));
}
notifyDaemonDidStart(port: string): void {

View File

@@ -1,14 +1,15 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import * as fs from 'fs';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { promises as fs, realpath, lstat, Stats, constants, rm } from 'fs';
import * as os from 'os';
import * as temp from 'temp';
import * as path from 'path';
import * as glob from 'glob';
import * as crypto from 'crypto';
import * as PQueue from 'p-queue';
import { ncp } from 'ncp';
import { promisify } from 'util';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { ConfigServiceImpl } from './config-service-impl';
import {
SketchesService,
@@ -24,8 +25,6 @@ import {
ArchiveSketchRequest,
LoadSketchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
import { duration } from '../common/decorators';
import * as glob from 'glob';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ServiceError } from './service-error';
import {
@@ -33,6 +32,19 @@ import {
maybeNormalizeDrive,
TempSketchPrefix,
} from './is-temp-sketch';
import { join } from 'path';
const RecentSketches = 'recent-sketches.json';
const DefaultIno = `void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
`;
@injectable()
export class SketchesServiceImpl
@@ -41,6 +53,16 @@ export class SketchesServiceImpl
{
private sketchSuffixIndex = 1;
private lastSketchBaseName: string;
private recentSketches: SketchWithDetails[] | undefined;
private readonly markAsRecentSketchQueue = new PQueue({
autoStart: true,
concurrency: 1,
});
private inoContent: Deferred<string> | undefined;
@inject(ILogger)
@named('sketches-service')
private readonly logger: ILogger;
@inject(ConfigServiceImpl)
private readonly configService: ConfigServiceImpl;
@@ -54,28 +76,7 @@ export class SketchesServiceImpl
@inject(IsTempSketch)
private readonly isTempSketch: IsTempSketch;
async getSketches({
uri,
exclude,
}: {
uri?: string;
exclude?: string[];
}): Promise<SketchContainer> {
const [/*old,*/ _new] = await Promise.all([
// this.getSketchesOld({ uri, exclude }),
this.getSketchesNew({ uri, exclude }),
]);
return _new;
}
@duration()
async getSketchesNew({
uri,
exclude,
}: {
uri?: string;
exclude?: string[];
}): Promise<SketchContainer> {
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
const root = await this.root(uri);
const pathToAllSketchFiles = await new Promise<string[]>(
(resolve, reject) => {
@@ -138,7 +139,7 @@ export class SketchesServiceImpl
for (const pathToSketchFile of pathToAllSketchFiles) {
const relative = path.relative(root, pathToSketchFile);
if (!relative) {
console.warn(
this.logger.warn(
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
);
continue;
@@ -146,7 +147,7 @@ export class SketchesServiceImpl
const segments = relative.split(path.sep);
if (segments.length < 2) {
// folder name, and sketch name.
console.warn(
this.logger.warn(
`Expected at least one segment relative path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Segments were: ${segments}.`
);
continue;
@@ -160,7 +161,7 @@ export class SketchesServiceImpl
''
);
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
console.warn(
this.logger.warn(
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
);
continue;
@@ -169,7 +170,7 @@ export class SketchesServiceImpl
if (child) {
child.sketches.push({
name: sketchName,
uri: FileUri.create(pathToSketchFile).toString(),
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
});
}
}
@@ -191,36 +192,40 @@ export class SketchesServiceImpl
const requestSketchPath = FileUri.fsPath(uri);
req.setSketchPath(requestSketchPath);
req.setInstance(instance);
const stat = new Deferred<fs.Stats | Error>();
fs.lstat(requestSketchPath, (err, result) =>
const stat = new Deferred<Stats | Error>();
lstat(requestSketchPath, (err, result) =>
err ? stat.resolve(err) : stat.resolve(result)
);
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
client.loadSketch(req, async (err, resp) => {
if (err) {
reject(
isNotFoundError(err)
? SketchesError.NotFound(
fixErrorMessage(
err,
requestSketchPath,
this.configService.cliConfiguration?.directories.user
),
uri
)
: err
);
let rejectWith: unknown = err;
if (isNotFoundError(err)) {
const invalidMainSketchFilePath = await isInvalidSketchNameError(
err,
requestSketchPath
);
if (invalidMainSketchFilePath) {
rejectWith = SketchesError.InvalidName(
err.details,
FileUri.create(invalidMainSketchFilePath).toString()
);
} else {
rejectWith = SketchesError.NotFound(err.details, uri);
}
}
reject(rejectWith);
return;
}
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
if (requestSketchPath !== responseSketchPath) {
console.warn(
this.logger.warn(
`Warning! The request sketch path was different than the response sketch path from the CLI. This could be a potential bug. Request: <${requestSketchPath}>, response: <${responseSketchPath}>.`
);
}
const resolvedStat = await stat.promise;
if (resolvedStat instanceof Error) {
console.error(
this.logger.error(
`The CLI could load the sketch from ${requestSketchPath}, but stating the folder has failed.`
);
reject(resolvedStat);
@@ -254,89 +259,163 @@ export class SketchesServiceImpl
private get recentSketchesFsPath(): Promise<string> {
return this.envVariableServer
.getConfigDirUri()
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
.then((uri) => path.join(FileUri.fsPath(uri), RecentSketches));
}
private async loadRecentSketches(
fsPath: string
): Promise<Record<string, number>> {
private async loadRecentSketches(): Promise<Record<string, number>> {
this.logger.debug(`>>> Loading recently opened sketches data.`);
const fsPath = await this.recentSketchesFsPath;
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, {
const raw = await fs.readFile(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch {}
try {
data = JSON.parse(raw);
} catch (err) {
this.logger.error(
`Could not parse recently opened sketches. Raw input was: ${raw}`
);
}
} catch (err) {
if ('code' in err && err.code === 'ENOENT') {
this.logger.debug(
`<<< '${RecentSketches}' does not exist yet. This is normal behavior. Falling back to empty data.`
);
return {};
}
throw err;
}
this.logger.debug(
`<<< Successfully loaded recently opened sketches data: ${JSON.stringify(
data
)}`
);
return data;
}
async markAsRecentlyOpened(uri: string): Promise<void> {
let sketch: Sketch | undefined = undefined;
try {
sketch = await this.loadSketch(uri);
} catch {
return;
}
if (await this.isTemp(sketch)) {
return;
}
const fsPath = await this.recentSketchesFsPath;
const data = await this.loadRecentSketches(fsPath);
const now = Date.now();
data[sketch.uri] = now;
let toDeleteUri: string | undefined = undefined;
if (Object.keys(data).length > 10) {
let min = Number.MAX_SAFE_INTEGER;
for (const uri of Object.keys(data)) {
if (min > data[uri]) {
min = data[uri];
toDeleteUri = uri;
}
}
}
if (toDeleteUri) {
delete data[toDeleteUri];
}
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
this.recentlyOpenedSketches().then((sketches) =>
this.notificationService.notifyRecentSketchesDidChange({ sketches })
private async saveRecentSketches(
data: Record<string, number>
): Promise<void> {
this.logger.debug(
`>>> Saving recently opened sketches data: ${JSON.stringify(data)}`
);
const fsPath = await this.recentSketchesFsPath;
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
this.logger.debug('<<< Successfully saved recently opened sketches data.');
}
async recentlyOpenedSketches(): Promise<Sketch[]> {
const configDirUri = await this.envVariableServer.getConfigDirUri();
const fsPath = path.join(
FileUri.fsPath(configDirUri),
'recent-sketches.json'
);
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch {}
async markAsRecentlyOpened(uri: string): Promise<void> {
return this.markAsRecentSketchQueue.add(async () => {
this.logger.debug(`Marking sketch at '${uri}' as recently opened.`);
if (this.isTempSketch.is(FileUri.fsPath(uri))) {
this.logger.debug(
`Sketch at '${uri}' is pointing to a temp location. Not marking as recently opened.`
);
return;
}
const sketches: SketchWithDetails[] = [];
for (const uri of Object.keys(data).sort(
(left, right) => data[right] - data[left]
)) {
let sketch: Sketch | undefined = undefined;
try {
const sketch = await this.loadSketch(uri);
sketches.push(sketch);
} catch {}
}
sketch = await this.loadSketch(uri);
this.logger.debug(
`Loaded sketch ${JSON.stringify(
sketch
)} before marking it as recently opened.`
);
} catch (err) {
if (
SketchesError.NotFound.is(err) ||
SketchesError.InvalidName.is(err)
) {
this.logger.debug(
`Could not load sketch from '${uri}'. Not marking as recently opened.`
);
return;
}
this.logger.error(
`Unexpected error occurred while loading sketch from '${uri}'.`,
err
);
throw err;
}
return sketches;
const data = await this.loadRecentSketches();
const now = Date.now();
this.logger.debug(
`Marking sketch '${uri}' as recently opened with timestamp: '${now}'.`
);
data[sketch.uri] = now;
let toDelete: [string, number] | undefined = undefined;
if (Object.keys(data).length > 10) {
let min = Number.MAX_SAFE_INTEGER;
for (const [uri, timestamp] of Object.entries(data)) {
if (min > timestamp) {
min = data[uri];
toDelete = [uri, timestamp];
}
}
}
if (toDelete) {
const [toDeleteUri] = toDelete;
delete data[toDeleteUri];
this.logger.debug(
`Deleted sketch entry ${JSON.stringify(
toDelete
)} from recently opened.`
);
}
await this.saveRecentSketches(data);
this.logger.debug(`Marked sketch '${uri}' as recently opened.`);
const sketches = await this.recentlyOpenedSketches(data);
this.notificationService.notifyRecentSketchesDidChange({ sketches });
});
}
async recentlyOpenedSketches(
forceUpdate?: Record<string, number> | boolean
): Promise<Sketch[]> {
if (!this.recentSketches || forceUpdate) {
const data =
forceUpdate && typeof forceUpdate === 'object'
? forceUpdate
: await this.loadRecentSketches();
const sketches: SketchWithDetails[] = [];
let needsUpdate = false;
for (const uri of Object.keys(data).sort(
(left, right) => data[right] - data[left]
)) {
let sketch: SketchWithDetails | undefined = undefined;
try {
sketch = await this.loadSketch(uri);
} catch {}
if (!sketch) {
needsUpdate = true;
} else {
sketches.push(sketch);
}
}
if (needsUpdate) {
const data = sketches.reduce((acc, curr) => {
acc[curr.uri] = curr.mtimeMs;
return acc;
}, {} as Record<string, number>);
await this.saveRecentSketches(data);
this.notificationService.notifyRecentSketchesDidChange({ sketches });
}
this.recentSketches = sketches;
}
return this.recentSketches;
}
async cloneExample(uri: string): Promise<Sketch> {
const sketch = await this.loadSketch(uri);
const parentPath = await this.createTempFolder();
const [sketch, parentPath] = await Promise.all([
this.loadSketch(uri),
this.createTempFolder(),
]);
const destinationUri = FileUri.create(
path.join(parentPath, sketch.name)
).toString();
@@ -377,7 +456,7 @@ export class SketchesServiceImpl
this.sketchSuffixIndex++
)}`;
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
const sketchExists = await promisify(fs.exists)(
const sketchExists = await this.exists(
path.join(sketchbookPath, sketchNameCandidate)
);
if (!sketchExists) {
@@ -393,21 +472,11 @@ export class SketchesServiceImpl
const sketchDir = path.join(parentPath, sketchName);
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
await promisify(fs.mkdir)(sketchDir, { recursive: true });
await promisify(fs.writeFile)(
sketchFile,
`void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
`,
{ encoding: 'utf8' }
);
const [inoContent] = await Promise.all([
this.loadInoContent(),
fs.mkdir(sketchDir, { recursive: true }),
]);
await fs.writeFile(sketchFile, inoContent, { encoding: 'utf8' });
return this.loadSketch(FileUri.create(sketchDir).toString());
}
@@ -424,7 +493,7 @@ void loop() {
reject(createError);
return;
}
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => {
realpath.native(dirPath, (resolveError, resolvedDirPath) => {
if (resolveError) {
reject(resolveError);
return;
@@ -462,7 +531,7 @@ void loop() {
const sketch = await this.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
if (SketchesError.NotFound.is(err) || SketchesError.InvalidName.is(err)) {
return undefined;
}
throw err;
@@ -478,7 +547,7 @@ void loop() {
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const exists = await promisify(fs.exists)(source);
const exists = await this.exists(source);
if (!exists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
@@ -503,7 +572,7 @@ void loop() {
);
const newPath = path.join(destinationPath, `${newName}.ino`);
if (oldPath !== newPath) {
await promisify(fs.rename)(oldPath, newPath);
await fs.rename(oldPath, newPath);
}
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
resolve();
@@ -520,7 +589,7 @@ void loop() {
const destination = FileUri.fsPath(destinationUri);
let tempDestination = await this.createTempFolder();
tempDestination = path.join(tempDestination, sketch.name);
await fs.promises.mkdir(tempDestination, { recursive: true });
await fs.mkdir(tempDestination, { recursive: true });
await copy(source, tempDestination);
await copy(tempDestination, destination);
return FileUri.create(destination).toString();
@@ -531,8 +600,8 @@ void loop() {
const { client } = await this.coreClient;
const archivePath = FileUri.fsPath(destinationUri);
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
if (await promisify(fs.exists)(archivePath)) {
await promisify(fs.unlink)(archivePath);
if (await this.exists(archivePath)) {
await fs.unlink(archivePath);
}
const req = new ArchiveSketchRequest();
req.setSketchPath(FileUri.fsPath(sketch.uri));
@@ -556,7 +625,7 @@ void loop() {
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
const sketchPath = FileUri.fsPath(sketch.uri);
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
}
@@ -564,57 +633,148 @@ void loop() {
async deleteSketch(sketch: Sketch): Promise<void> {
return new Promise<void>((resolve, reject) => {
const sketchPath = FileUri.fsPath(sketch.uri);
fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
if (error) {
console.error(`Failed to delete sketch at ${sketchPath}.`, error);
this.logger.error(`Failed to delete sketch at ${sketchPath}.`, error);
reject(error);
} else {
console.log(`Successfully deleted sketch at ${sketchPath}.`);
this.logger.info(`Successfully deleted sketch at ${sketchPath}.`);
resolve();
}
});
});
}
private async exists(pathLike: string): Promise<boolean> {
try {
await fs.access(pathLike, constants.R_OK | constants.W_OK);
return true;
} catch {
return false;
}
}
// Returns the default.ino from the settings or from default folder.
private async readSettings(): Promise<Record<string, unknown> | undefined> {
const configDirUri = await this.envVariableServer.getConfigDirUri();
const configDirPath = FileUri.fsPath(configDirUri);
try {
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
encoding: 'utf8',
});
return this.tryParse(raw);
} catch (err) {
if ('code' in err && err.code === 'ENOENT') {
return undefined;
}
throw err;
}
}
private tryParse(raw: string): Record<string, unknown> | undefined {
try {
return JSON.parse(raw);
} catch {
return undefined;
}
}
// Returns the default.ino from the settings or from default folder.
private async loadInoContent(): Promise<string> {
if (!this.inoContent) {
this.inoContent = new Deferred<string>();
const settings = await this.readSettings();
if (settings) {
const inoBlueprintPath = settings['arduino.sketch.inoBlueprint'];
if (inoBlueprintPath && typeof inoBlueprintPath === 'string') {
try {
const inoContent = await fs.readFile(inoBlueprintPath, {
encoding: 'utf8',
});
this.inoContent.resolve(inoContent);
} catch (err) {
if ('code' in err && err.code === 'ENOENT') {
// Ignored. The custom `.ino` blueprint file is optional.
} else {
throw err;
}
}
}
}
this.inoContent.resolve(DefaultIno);
}
return this.inoContent.promise;
}
}
interface SketchWithDetails extends Sketch {
readonly mtimeMs: number;
}
// https://github.com/arduino/arduino-cli/issues/1797
function fixErrorMessage(
err: ServiceError,
sketchPath: string,
sketchbookPath: string | undefined
): string {
if (!sketchbookPath) {
return err.details; // No way to repair the error message. The current sketchbook path is not available.
}
// Original: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing /Users/a.kitta/Documents/Arduino/Arduino.ino`
// Fixed: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing $sketchPath`
const message = err.details;
const incorrectMessageSuffix = path.join(sketchbookPath, 'Arduino.ino');
if (
message.startsWith("Can't open sketch: no valid sketch found in") &&
message.endsWith(`${incorrectMessageSuffix}`)
) {
const sketchName = path.basename(sketchPath);
const correctMessagePrefix = message.substring(
0,
message.length - incorrectMessageSuffix.length
);
return `${correctMessagePrefix}${path.join(
sketchPath,
`${sketchName}.ino`
)}`;
}
return err.details;
}
function isNotFoundError(err: unknown): err is ServiceError {
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
}
/**
* Tries to detect whether the error was caused by an invalid main sketch file name.
* IDE2 should handle gracefully when there is an invalid sketch folder name. See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762), so IDE2 parses the error message and tries to guess it.
* Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
*/
async function isInvalidSketchNameError(
cliErr: unknown,
requestSketchPath: string
): Promise<string | undefined> {
if (isNotFoundError(cliErr)) {
const ino = requestSketchPath.endsWith('.ino');
if (ino) {
const sketchFolderPath = path.dirname(requestSketchPath);
const sketchName = path.basename(sketchFolderPath);
const pattern = `${invalidSketchNameErrorRegExpPrefix}${path.join(
sketchFolderPath,
`${sketchName}.ino`
)}`.replace(/\\/g, '\\\\'); // make windows path separator with \\ to have a valid regexp.
if (new RegExp(pattern, 'i').test(cliErr.details)) {
try {
await fs.access(requestSketchPath);
return requestSketchPath;
} catch {
return undefined;
}
}
} else {
try {
const resources = await fs.readdir(requestSketchPath, {
withFileTypes: true,
});
return (
resources
.filter((resource) => resource.isFile())
.filter((resource) => resource.name.endsWith('.ino'))
// A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much,
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
.sort(({ name: left }, { name: right }) =>
left.localeCompare(right)
)
.map(({ name }) => name)
.map((name) => path.join(requestSketchPath, name))[0]
);
} catch (err) {
if ('code' in err && err.code === 'ENOTDIR') {
return undefined;
}
throw err;
}
}
}
return undefined;
}
const invalidSketchNameErrorRegExpPrefix =
'.*: main file missing from sketch: ';
/*
* When a new sketch is created, add a suffix to distinguish it
* from other new sketches I created today.

View File

@@ -0,0 +1,136 @@
import URI from '@theia/core/lib/common/uri';
import { expect } from 'chai';
import { BoardSearch, LibrarySearch, Searchable } from '../../common/protocol';
interface Expectation<S extends Searchable.Options> {
readonly uri: string;
readonly expected: S | undefined | string;
}
describe('searchable', () => {
describe('parse', () => {
describe(BoardSearch.UriParser.authority, () => {
(
[
{
uri: 'http://boardsmanager#SAMD',
expected: { query: 'SAMD', type: 'All' },
},
{
uri: 'http://boardsmanager/Arduino%40Heart#littleBits',
expected: { query: 'littleBits', type: 'Arduino@Heart' },
},
{
uri: 'http://boardsmanager/too/many/segments#invalidPath',
expected: undefined,
},
{
uri: 'http://boardsmanager/random#invalidPath',
expected: undefined,
},
{
uri: 'https://boardsmanager/#invalidScheme',
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://boardsmanager/#invalidScheme.`,
},
{
uri: 'http://librarymanager/#invalidAuthority',
expected: `Invalid 'authority'. Expected: 'boardsmanager'. URI was: http://librarymanager/#invalidAuthority.`,
},
] as Expectation<BoardSearch>[]
).map((expectation) => toIt(expectation, BoardSearch.UriParser.parse));
});
describe(LibrarySearch.UriParser.authority, () => {
(
[
{
uri: 'http://librarymanager#WiFiNINA',
expected: { query: 'WiFiNINA', type: 'All', topic: 'All' },
},
{
uri: 'http://librarymanager/All/Device%20Control#Servo',
expected: {
query: 'Servo',
type: 'All',
topic: 'Device Control',
},
},
{
uri: 'http://librarymanager/All/Display#SparkFun',
expected: {
query: 'SparkFun',
type: 'All',
topic: 'Display',
},
},
{
uri: 'http://librarymanager/Updatable/Display#SparkFun',
expected: {
query: 'SparkFun',
type: 'Updatable',
topic: 'Display',
},
},
{
uri: 'http://librarymanager/All/Signal%20Input%2FOutput#debouncer',
expected: {
query: 'debouncer',
type: 'All',
topic: 'Signal Input/Output',
},
},
{
uri: 'http://librarymanager/too/many/segments#invalidPath',
expected: undefined,
},
{
uri: 'http://librarymanager/absent/invalid#invalidPath',
expected: undefined,
},
{
uri: 'https://librarymanager/#invalidScheme',
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://librarymanager/#invalidScheme.`,
},
{
uri: 'http://boardsmanager/#invalidAuthority',
expected: `Invalid 'authority'. Expected: 'librarymanager'. URI was: http://boardsmanager/#invalidAuthority.`,
},
] as Expectation<LibrarySearch>[]
).map((expectation) => toIt(expectation, LibrarySearch.UriParser.parse));
});
});
});
function toIt<S extends Searchable.Options>(
{ uri, expected }: Expectation<S>,
run: (uri: URI) => Searchable.Options | undefined
): Mocha.Test {
return it(`should ${
typeof expected === 'string'
? `fail to parse '${uri}'`
: !expected
? `not parse '${uri}'`
: `parse '${uri}' to ${JSON.stringify(expected)}`
}`, () => {
if (typeof expected === 'string') {
try {
run(new URI(uri));
expect.fail(
`Expected an error with message '${expected}' when parsing URI: ${uri}.`
);
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).to.be.equal(expected);
}
} else {
const actual = run(new URI(uri));
if (!expected) {
expect(actual).to.be.undefined;
} else {
expect(actual).to.be.deep.equal(
expected,
`Was: ${JSON.stringify(actual)}`
);
}
}
});
}

View File

@@ -11,14 +11,14 @@ There are several ways you can get involved:
| - Support<br/>- Question<br/>- Discussion | Post on the [**Arduino Forum**][forum] |
| - Bug report<br/>- Feature request | Issue report (see the guide [**here**][issues]) |
| Testing | Beta testing, PR review (see the guide [**here**][beta-testing]) |
| Translation | [Transifex project][translate] |
| Translation | See the guide [**here**][translate] |
| - Bug fix<br/>- Enhancement | Pull request (see the guide [**here**][prs]) |
| Monetary | - [Donate][donate]<br/>- [Sponsor][sponsor]<br/>- [Buy official products][store] |
[forum]: https://forum.arduino.cc
[issues]: contributor-guide/issues.md#issue-report-guide
[beta-testing]: contributor-guide/beta-testing.md#beta-testing-guide
[translate]: https://www.transifex.com/arduino-1/ide2/dashboard/
[translate]: contributor-guide/translation.md#translator-guide
[prs]: contributor-guide/pull-requests.md#pull-request-guide
[donate]: https://www.arduino.cc/en/donate/
[sponsor]: https://github.com/sponsors/arduino

View File

@@ -0,0 +1,33 @@
# Translator Guide
The text of the Arduino IDE interface is translated into several languages. The language can be selected in the dialog opened via **File > Preferences** in the Arduino IDE menus (**Arduino IDE > Preferences** for macOS users).
Translating text and improving on existing translations is a valuable contribution to the project, helping make Arduino accessible to everyone.
The translations for the text found in the Arduino IDE come from several sources:
## Arduino IDE Text
Translations of Arduino IDE's text is done in the "**Arduino IDE 2.0**" project on the **Transifex** localization platform:
https://explore.transifex.com/arduino-1/ide2/
## Base Application Text
Arduino IDE leverages the localization data available for the [**VS Code**](https://code.visualstudio.com/) editor to localize shared UI text. This reduces the translation work required to add a new language to the text specific to the Arduino IDE project.
For this reason, some of Arduino IDE's text is not found in the **Transifex** project. Suggestions for corrections or improvement to this text are made by submitting an issue to the `microsoft/vscode-loc` GitHub repository.
Before submitting an issue, please check the existing issues to make sure it wasn't already reported:<br />
https://github.com/microsoft/vscode-loc/issues
After that, submit an issue here:<br />
https://github.com/microsoft/vscode-loc/issues/new
## Arduino CLI Text
The [**Arduino CLI**](https://arduino.github.io/arduino-cli/latest/) tool handles non-GUI operations for the Arduino IDE. Some of the text printed in the "**Output**" panel and in notifications originates from **Arduino CLI**.
Translations of Arduino CLI's text is done in the "**Arduino CLI**" Transifex project:
https://explore.transifex.com/arduino-1/arduino-cli/

View File

@@ -1,6 +1,10 @@
# Release Procedure
## 🗺️ Merge localization sync PR
## Steps
The following are the steps to follow to make a release of Arduino IDE:
### 1. 🗺️ Merge localization sync PR
A pull request titled "**Update translation files**" is submitted periodically by the "**github-actions**" bot to pull in the localization data from [**Transifex**](https://www.transifex.com/arduino-1/ide2/dashboard/).
@@ -10,24 +14,24 @@ It will be shown in these search results:
https://github.com/arduino/arduino-ide/pulls/app%2Fgithub-actions
## ⚙ Create the release on GitHub
### 2. 👀 Check version of packages
First of all, you need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, and `./electron-app/package.json`), create a PR, and merge it on the `main` branch.
The [`version` field](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#version) of the project's `package.json` metadata files received a patch version bump (e.g., `2.0.1` -> `2.0.2`) at the time of the previous release.
To do so, you can make use of the `update:version` script.
If this is a patch release, the current metadata values are correct and no action is needed.
For example, if you want to release the version `<YOUR_VERSION>`, you should run the following commands:
The changes contained in this release might be considered to change the project's "API". If so, a patch version bump will not be appropriate and the version must be adjusted in compliance with the [**Semantic Versioning Specification**](https://semver.org/).
```text
git checkout main
git pull
git checkout -b version-<YOUR_VERSION>
yarn update:version <YOUR_VERSION>
git commit -am <YOUR_VERSION>
git push origin version-<YOUR_VERSION>
```
Follow the instructions for updating the version metadata [**here**](#update-version-metadata).
replacing `<YOUR_VERSION>` with the version you want to release. Then create a PR and merge it.
#### Examples
If the version number of the previous release was `2.0.1`:
- If this is considered a minor release (non-breaking changes to the "API"), the `version` values must be changed to `2.1.0`.
- If this is considered a major release (breaking changes to the "API"), the `version` values must be changed to `3.0.0`.
### 3. 🚢 Create the release on GitHub
Then, you need to **create and push the new tag** and wait for the release to appear on [the "**Releases**" page](https://github.com/arduino/arduino-ide/releases).
@@ -42,7 +46,13 @@ git push origin <YOUR_VERSION>
Pushing a tag will trigger a **GitHub Actions** workflow on the `main` branch. Check the "**Arduino IDE**" workflow and see that everything goes right. If the workflow succeeds, a new release will be created automatically and you should see it on the ["**Releases**"](https://github.com/arduino/arduino-ide/releases) page.
## 📄 Create the changelog
### 4. ⬆️ Bump version metadata of packages
In order for the version number of the tester and nightly builds to have correct precedence compared to the release version, the `version` field of the project's `package.json` files must be given a patch version bump (e.g., `2.0.1` -> `2.0.2`) **after** the creation of the release tag.
Follow the instructions for updating the version metadata [**here**](#update-version-metadata).
### 5. 📄 Create the changelog
**Create GitHub issues for the known issues** that we haven't solved in the current release:
@@ -61,7 +71,7 @@ Add a list of mentions of GitHub users who contributed to the release in any of
Add a "**Known Issues**" section at the bottom of the changelog.
## ✎ Update the "**Software**" Page
### 6. ✎ Update the "**Software**" Page
Open a PR on the [bcmi-labs/wiki-content](https://github.com/bcmi-labs/wiki-content) repository to update the links and texts.
@@ -78,7 +88,7 @@ When the deploy workflow is done, check if links on the "**Software**" page are
https://www.arduino.cc/en/software#future-version-of-the-arduino-ide
## 😎 Brag about it
### 7. 😎 Brag about it
- Ask in the `#product_releases` **Slack** channel to write a post for the social media and, if needed, a blog post.
- Post a message on the forum (ask @per1234).<br />
@@ -97,3 +107,28 @@ https://www.arduino.cc/en/software#future-version-of-the-arduino-ide
>
> To see the details, you can take a look at the [Changelog](https://github.com/arduino/arduino-ide/releases/tag/2.0.0-beta.12)
> If you want to post about it on social media and you need more details feel free to ask us on #team_tooling! :wink:
## Operations
The following are detailed descriptions of operations performed during the release process:
<a id="update-version-metadata"></a>
### ⚙ Update version metadata of packages
You need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, and `./electron-app/package.json`), create a PR, and merge it on the `main` branch.
To do so, you can make use of the `update:version` script.
For example, if you want to update the version to `<YOUR_VERSION>`, you should run the following commands:
```text
git checkout main
git pull
git checkout -b version-<YOUR_VERSION>
yarn update:version <YOUR_VERSION>
git commit -am <YOUR_VERSION>
git push origin version-<YOUR_VERSION>
```
replacing `<YOUR_VERSION>` with the version you want. Then create a PR and merge it.

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "electron-app",
"version": "2.0.0",
"version": "2.0.1",
"license": "AGPL-3.0-or-later",
"main": "src-gen/frontend/electron-main.js",
"dependencies": {
@@ -21,7 +21,7 @@
"@theia/process": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"arduino-ide-extension": "2.0.0"
"arduino-ide-extension": "2.0.1"
},
"devDependencies": {
"@theia/cli": "1.25.0",
@@ -48,6 +48,7 @@
"comments": false,
"strings": false
},
"editor.maxTokenizationLineLength": 500,
"breadcrumbs.enabled": false,
"workbench.tree.renderIndentGuides": "none",
"explorer.compactFolders": false

View File

@@ -17,6 +17,53 @@ const {
FrontendApplicationConfigProvider,
} = require('@theia/core/lib/browser/frontend-application-config-provider');
function fetchFrom(path) {
const { Endpoint } = require('@theia/core/lib/browser/endpoint');
const endpoint = new Endpoint({ path }).getRestUrl().toString();
return fetch(endpoint);
}
async function loadTranslations() {
const { nls } = require('@theia/core/lib/common/nls');
const defaultLocale = typeof window === 'object' && window && window.localStorage.getItem(nls.localeId) || '';
if (defaultLocale && !nls.locale) {
Object.assign(nls, {
locale: defaultLocale
});
}
if (nls.locale) {
const response = await fetchFrom(`/i18n/${nls.locale}`);
nls.localization = await response.json();
}
}
async function loadBackendOS() {
const response = await fetchFrom('/os');
const osType = await response.text();
const isWindows = osType === 'Windows';
const isOSX = osType === 'OSX';
OS.backend.isOSX = isOSX;
OS.backend.isWindows = isWindows;
OS.backend.type = () => osType;
}
function customizeMonacoNls() {
const MonacoNls = require('@theia/monaco-editor-core/esm/vs/nls');
const { nls: TheiaNls } = require('@theia/core/lib/common/nls');
const { Localization } = require('@theia/core/lib/common/i18n/localization');
Object.assign(MonacoNls, {
localize(_, label, ...args) {
if (TheiaNls.locale) {
const defaultKey = TheiaNls.getDefaultKey(label);
if (defaultKey) {
return TheiaNls.localize(defaultKey, label, ...args);
}
}
return Localization.format(label, args);
}
});
}
// It is a mighty hack to support theme updates in the bundled IDE2.
// If the custom theme registration happens before the restoration of the existing monaco themes, then any custom theme changes will be ignored.
// This patch introduces a static deferred promise in the monaco-theming service that will be resolved when the restoration is ready.
@@ -25,8 +72,14 @@ const {
// This patch customizes the monaco theme service behavior before loading the DI containers via the preload.
// The preload is called only once before the app loads. The Theia extensions are not loaded at that point, but the app config provider is ready.
const preloader = require('@theia/core/lib/browser/preloader');
const originalPreload = preloader.preload;
preloader.preload = async function () {
// Must require the monaco frontend module to activate the NLS customization for monaco.
// Otherwise, the NLS customization would trigger after the monaco UI components with all their translations are already loaded.
await Promise.allSettled([
loadTranslations(),
loadBackendOS(),
]);
customizeMonacoNls();
const { MonacoThemingService } = require('@theia/monaco/lib/browser/monaco-theming-service');
const { MonacoThemeServiceIsReady } = require('arduino-ide-extension/lib/browser/utils/window');
const { Deferred } = require('@theia/core/lib/common/promise-util');
@@ -42,7 +95,6 @@ preloader.preload = async function () {
await this.restore();
ready.resolve();
}.bind(MonacoThemingService);
return originalPreload();
}.bind(preloader);
const lightTheme = 'arduino-theme';

View File

@@ -137,24 +137,5 @@
"path": "arduino-ide/nightly"
}
]
},
"theiaPluginsDir": "plugins",
"theiaPlugins": {
"vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix",
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.5.vsix",
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
"cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix",
"vscode-language-pack-nl": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-nl/1.48.3/file/MS-CEINTL.vscode-language-pack-nl-1.48.3.vsix",
"vscode-language-pack-fr": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-fr/1.69.0/file/MS-CEINTL.vscode-language-pack-fr-1.69.0.vsix",
"vscode-language-pack-zh-hans": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-zh-hans/1.69.0/file/MS-CEINTL.vscode-language-pack-zh-hans-1.69.0.vsix",
"vscode-language-pack-de": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-de/1.69.0/file/MS-CEINTL.vscode-language-pack-de-1.69.0.vsix",
"vscode-language-pack-ja": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-ja/1.69.0/file/MS-CEINTL.vscode-language-pack-ja-1.69.0.vsix",
"vscode-language-pack-tr": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-tr/1.69.0/file/MS-CEINTL.vscode-language-pack-tr-1.69.0.vsix",
"vscode-language-pack-it": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-it/1.69.0/file/MS-CEINTL.vscode-language-pack-it-1.69.0.vsix",
"vscode-language-pack-ru":"https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-ru/1.69.0/file/MS-CEINTL.vscode-language-pack-ru-1.69.0.vsix",
"vscode-language-pack-es": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-es/1.69.0/file/MS-CEINTL.vscode-language-pack-es-1.69.0.vsix",
"vscode-language-pack-pt-BR": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-pt-BR/1.69.0/file/MS-CEINTL.vscode-language-pack-pt-BR-1.69.0.vsix",
"vscode-language-pack-cs": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-cs/1.69.0/file/MS-CEINTL.vscode-language-pack-cs-1.69.0.vsix"
}
}

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