Compare commits

..

136 Commits

Author SHA1 Message Date
Akos Kitta
88cd571080 fix: restored breadcrumbs.enabled
Ref: #1733
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-15 14:46:12 +01:00
Akos Kitta
9ffe421fab fix: restored window.titleBarStyle
Ref: #1733

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-15 14:32:08 +01:00
Akos Kitta
d68bc4abdb feat: rename, deletion, and validation support
Closes #1599
Closes #1825
Closes #649
Closes #1847
Closes #1882

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
Co-authored-by: per1234 <accounts@perglass.com>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-15 14:09:36 +01:00
github-actions[bot]
4f07515ee8 Updated themes (#1836)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-15 09:27:23 +01:00
Akos Kitta
25b545d4c4 fix: added GH token for vscode-ripgrep download
Otherwise, yarn install hits an HTTP 403 due to the rate-limiter.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-13 15:28:39 +01:00
Akos Kitta
79b6b7ecc0 fix: library search boosting
Closes #1106

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-10 14:29:26 +01:00
Akos Kitta
5d264ef5b6 fix: show board info based on the selected port
include serial number of board if available

Closes #1489
Closes #1435

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-02-06 09:20:44 +01:00
dependabot[bot]
f63ee85fa3 build(deps): Bump svenstaro/upload-release-action from 2.4.0 to 2.4.1
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.4.0...2.4.1)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-05 07:55:22 -08:00
per1234
083a7069f0 Add leading+trailing line break to "Copy for Forum" content
Arduino IDE's "Edit > Copy for Forum (Markdown)" feature copies the contents of the currently selected editor tab to the
clipboard, with "fenced code block" markup added to provide correct formatting when the content is posted to a platform
supporting Markdown language such as Arduino Forum, GitHub, Stack Exchange, etc.

One of the most common points of friction between the volunteer helpers on the forum and the newcomers requesting
assistance is the failure to use the correct markup when posting code. In the best case scenario the code and thread is
made less readable and less convenient to copy to the IDE for further investigation. Components of the code often
resemble markup, which causes it to be corrupted by the forum's renderer.

Even in cases where the user was conscientious enough to attempt to add the right markup, which is facilitated by tools
such as "Copy for Forum (Markdown)", they often still don't get it quite right. So it is worthwhile to make efforts to
make it less likely for the markup to be inadvertently invalidated.

"Fenced code block" markup must be on its own lines before and after the code content. Someone not familiar with
Markdown won't know this fact and may simply paste the content copied via "Copy for Forum (Markdown)" without manually
adding a newline before and after. Since the code content is preceded and succeeded by a newline, they will not have any
visual indication of a problem.

Adding a newline before and after the content will ensure the markup is valid regardless of the context it is pasted
into. In cases where the user did add a newline and this introduces a redundant line break in the forum post, it will
not have any effect on the rendered content because the additional newlines are ignored by the renderer.
2023-02-03 04:58:15 -08:00
Akos Kitta
f5621db85d fix: flaky compiler test
- The gRPC core client provider requires an initialized config service when processing the error message received during the `InitRequest`
 - Additional logging in the config service.
 - The tests log into the console.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-27 10:23:51 +01:00
Akos Kitta
658f117e93 test: added compiler + build output path test
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-24 09:06:07 +01:00
Akos Kitta
6140ae525c feat: handle when starting debug session failed (#1809)
If the sketch has not been verified, IDE2 offers the user a verify action.

Closes #808

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-23 09:03:49 +01:00
dependabot[bot]
afb02da806 build(deps): Bump svenstaro/upload-release-action from 2.3.0 to 2.4.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.3.0...2.4.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-18 22:08:34 -08:00
Akos Kitta
692f29fe1a fix: sketchbook container building
Closes #1185

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-17 13:54:26 +01:00
Akos Kitta
40e797966f fix: start the LS with the board specific settings
restart the LS when board settings of the running LS has changed

Closes #1029

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-16 11:39:22 +01:00
Akos Kitta
a15a94a339 fix: workaround for # in the app path
Closes #1815
Ref eclipse-theia/theia#12064

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-16 09:43:20 +01:00
Akos Kitta
ca687cfe40 fix: remove unused module
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-16 09:43:20 +01:00
Akos Kitta
32e17745f1 chore: Use 0.7.4 LS with the URL encoding fixes
Closes #1124

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-16 09:43:20 +01:00
Akos Kitta
432f3654df fix: restart LS on lib/core change, client re-init
Closes #670

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-16 09:38:27 +01:00
Akos Kitta
197cea2a60 fix: aligned Add File... behavior with IDE 1.x
- code files will be copied to sketch folder root
 - other files go under the `data` folder in the sketch folder root

Closes #284

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-11 09:23:50 +01:00
Akos Kitta
b2bf368db9 fix: update Examples and Include Library menu after ZIP install
Closes #659

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-11 09:23:23 +01:00
Akos Kitta
287b2e3f41 feat: removed encoding from the status bar
Closes #1393

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-11 09:22:53 +01:00
Akos Kitta
da0fecfd0f feat: no remote fetch when IDE gets CLI version
the CLI version is retrieved from the `package.json` of the extension:
`arduino.cli.version`

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-01-11 09:22:29 +01:00
Akos Kitta
76f9f635d8 feat: configure sketchbook location without restart
Closes #1764
Closes #796
Closes #569
Closes #655

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-21 15:44:10 +01:00
Self Not Found
3f05396222 Replace socks with socks5 in proxy protocol (#1776)
The net/http package in arduino-cli supports socks5 as scheme rather than socks.

Co-authored-by: per1234 <accounts@perglass.com>
2022-12-21 14:34:09 +01:00
Self Not Found
644e6079b3 Remove trailing colon when parsing the protocol from URL (#1778)
* Remove trailing colon when parsing the protocol from URL
* Fix bug
2022-12-21 14:32:46 +01:00
Akos Kitta
1d342cdbd0 build: use a local npm registry for app packaging
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-15 12:29:26 +01:00
Akos Kitta
908ec4c544 fix: menu item enablement issues
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-15 09:21:45 +01:00
Frank Palazzolo
7c86f1f9d3 Fix corruption of incoming UTF-8-encoded serial data (#1758)
* Fix corruption of incoming UTF-8-encoded serial data
* Make TextDecoder() readonly because it can be

Co-authored-by: z3bra <111462146+z3bra5hax@users.noreply.github.com>
2022-12-13 09:01:51 +01:00
Akos Kitta
f8c01e379c fix: list view filtering when opening the view
When the widget is opened the first time with any search options,
the widget might miss the refresh event as it does not happen at
instantiation time. This PR ensures that all list widget refresh
events wait until the React component is rendered and is visible
in the UI.

This change also ensures that the debounced search will run with
the most recent search options by setting the `trailing` property
of the debounced function to `true`.

Closes #1740

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-08 08:17:23 +01:00
Akos Kitta
af468a73bc feat: show the selected board config value on menu
closes #343

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-08 08:16:02 +01:00
per1234
d3a863911c Restore certificate check compatibility w/ RC2-40-CBC encrypted PKS#12
The "Check Certificates" GitHub Actions workflow uses OpenSSL to check for problems with the project's signing
certificates.

Certificates exported to PKS#12 archive files using older tools may have been encrypted using the "RC2-40-CBC"
algorithm.

Due to the availability of more secure modern alternatives, default support for RC2-40-CBC encryption was dropped in
OpenSSL 3.x.

The macOS signing certificate uses this RC2-40-CBC encryption.

The "Check Certificates" GitHub Actions workflow runs on the `ubuntu-latest` runner. Previously, this runner used Ubuntu
20.04. This has now changed to Ubuntu 22.04. With the operating system update came an OpenSSL update from 1.1.1f to
3.0.2. This caused the workflow runs to fail on the macOS certificate job:

Error outputting keys and certificates
80FBB0C5087F0000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:../crypto/evp/evp_fetch.c:349:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()

Even though no longer done by default, OpenSSL still supports RC2-40-CBC encryption via its "legacy" provider. So
compatibility with the certificate is restored by adding the `-legacy` flag to the `openssl pkcs12` commands.
2022-12-07 03:09:40 -08:00
Akos Kitta
c4172ee8e1 fix: theme service binding
cleaned up unused symbol

closes #1742

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-07 09:52:12 +01:00
Akos Kitta
ed8ed15168 fix: Preferences menu enablement defect
Manually update the menus when executing the command.

Also disabled the menu when the settings dialog is opened.

closes #1735

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-07 09:51:55 +01:00
per1234
32f0426f01 Fix formatting of generated release notes
The "Arduino IDE" GitHub Actions workflow generates a changelog from the commits since the last tag.

This changelog is published in multiple ways:

- Printed to workflow run logs
- Uploaded to Arduino's download server (mostly useful for the nightly builds)
- Initial version of release notes

For the last, the changelog text must be passed from the dedicated changelog generation workflow step to the release
step. This is done via workflow job output.

At the time the system was set up, outputs for workflow `run` steps were set using the `set-output` workflow command.
That "workflow command" system was later determined by GitHub to have potential security vulnerabilities, so it was
replaced with a `GITHUB_OUTPUT` environment file.

The "Arduino IDE" workflow was migrated to the new "environment file" approach. It was later discovered that there was
an undocumented breaking change in the method for handling multi-line strings in workflow step outputs between the old
"workflow command" system and the new "environment file". This resulted in the initial release notes having an incorrect
format. For example, what would previously have been formatted like this:

- Updated translation files (#1606) [23c7f5f]
- Use 0.29.0 CLI in IDE2 (#1683) [f1144ef]

Was now formatted like this:

- Updated translation files (#1606) [23c7f5f]%0A - Use 0.29.0 CLI in IDE2 (#1683) [f1144ef]%0A

The solution is to remove the commands that did the escaping of the changelog text in a manner that is no longer
supported and replace them with a "here document"-style format.

A random number is used as the "delimiter" (limit string) per the security recommendations in the official GitHub
documentation. Note that even though the multiline strings handling documentation was placed under the environment
variable section, it also applies to setting outputs.
2022-12-07 00:44:21 -08:00
Alberto Iannaccone
200c00244b 2.0.4 2022-12-06 04:13:54 -08:00
github-actions[bot]
1104467329 Updated translation files (#1701)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-12-05 10:24:38 +01:00
Akos Kitta
5695fd8afb fix: filtered undesired contributions: RTOS view
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
d0e383853f feat: patched the Theia debug functionality
Patch for:
 - eclipse-theia/theia#11871
 - eclipse-theia/theia#11879
 - eclipse-theia/theia#11880
 - eclipse-theia/theia#11885
 - eclipse-theia/theia#11886
 - eclipse-theia/theia#11916

Closes #1582

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
3bc412b42f feat: Updated to cortex-debug@1.5.1
Closes #246

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
f553d6919d feat: no ping timeout in dev mode
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
d6a4b0f910 fix: update monitor settings only if it's changed
Closes #375

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 14:07:58 +01:00
Akos Kitta
c0488d1f64 fix: remote sketch creation if tree is not active
Closes #1715

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 11:19:13 +01:00
Akos Kitta
81195431b0 fix: double update of the zoom level on save
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:58:23 +01:00
Akos Kitta
87109e6559 chore: Switched to window.zoomLevel preference
Deprecated `arduino.window.zoomLevel`.

Closes #1657

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:58:23 +01:00
Akos Kitta
c0af1e62e8 fix: main sketch file editor focus on layout reset
Closes #643

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:51:19 +01:00
Akos Kitta
ac9cce16f7 chore: Updated to Theia 1.31.1 (#1662)
Closes #1655
Closes #1656

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 09:39:54 +01:00
Alberto Iannaccone
3ad660927f Fix keybindings to switch between tabs on MacOs (#1686) 2022-11-29 09:22:14 +01:00
Akos Kitta
8778d70ad7 fix: editor widget resolving when creating new tab
An already opened editor widget can resolve without waiting.

Closes #1718

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 09:20:16 +01:00
Alberto Iannaccone
fe3fbb189c 2.0.3 (#1687) 2022-11-17 15:03:22 +01:00
github-actions[bot]
23c7f5f848 Updated translation files (#1606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-11-17 12:47:21 +01:00
Alberto Iannaccone
f1144efb93 Use 0.29.0 CLI in IDE2 (#1683) 2022-11-17 12:43:28 +01:00
per1234
9cec643cab Fix nightly build links in issue forms
The issue forms are configured to request the contributor to test using the nightly build of Arduino IDE before
submitting an issue in order to make sure the bug or feature request has not already been resolved.

Some time ago, the repository's readme contained a table of download links. The links in the issue forms pointed there.
That table was replaced with a link to the official "Software" page in order to reduce unnecessary verbosity and
maintenance burden of the project's documentation content.

The issue form links were not updated at that time. The resulting additional link in the chain made obtaining the
nightly build less convenient for the contributor to obtain.

The links are hereby updated to point directly to the list of nightly build download links on the arduino.cc "Software"
page.
2022-11-17 03:25:26 -08:00
Akos Kitta
1a7784a540 feat: progress for the remote sketch creation
Closes #1668

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-17 11:05:34 +01:00
Akos Kitta
d24a3911f8 fix: workaround for arduino/arduino-cli#1968
Do not try to parse the original `NotFound` error message, but look for
a sketch somewhere in the requested path.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-11 13:19:08 +01:00
Akos Kitta
3735553003 fix: flawed timing issue when opening editors
From now on, the editor widget open promise resolution does not rely on
internal Theia events but solely on @phosphor's `isAttached`/`isVisible`
properties.
The editor widget promise resolves with the next task after a navigation
frame so the browser can render the widget.

Closes #1612

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-11 13:19:08 +01:00
Akos Kitta
f6d112e1f6 fix: escaped regex chars in pattern
Closes #1600

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 16:33:42 +01:00
Akos Kitta
cc2d557706 fix: relaxed condition to check if resource exists
The original (`fs-extra`-based) implementation did not check if the
file is writable either.

Resources are not writable in mounted AppImages.

Closes #1586

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:44 +01:00
Akos Kitta
103acc4b7e fix: allow second instance on macOS
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:17 +01:00
Akos Kitta
c3dc7c6307 fix: avoid ENOTDIR when opening second instance.
If the resource is a file, do not try to `readdir`, but return undefined

Closes #1590

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:17 +01:00
Akos Kitta
7d6a2d5e33 feat: Create remote sketch
Closes #1580

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:12:20 +01:00
Akos Kitta
6984c52b92 fix: Handle gracefully when trying to detect invalid sketch name error and folder is missing on filesystem (#1616)
- feat: generalized Node.js error handling
    - Gracefully handle when the sketch folder has been deleted
 - feat: spare detecting invalid sketch name error
    - The invalid sketch name detection requires at least one extra FS access.
       Do not try to detect the invalid sketch name error, but use the original
       `NotFound` from the CLI.
 - fix: typo

Closes #1596

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:11:35 +01:00
Akos Kitta
3a70547770 fix: do not trim stdout of clang-format process
Otherwise, it always causes a _no newline at the end of file_ problem.

Closes #1487

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-08 09:39:41 +01:00
per1234
8a85b5c3d8 Migrate workflows from deprecated set-output commands
GitHub Actions provides the capability for workflow authors to use the capabilities of the GitHub Actions ToolKit
package directly in the `run` keys of workflows via "workflow commands". One such command is `set-output`, which allows
data to be passed out of a workflow step as an output.

It has been determined that this command has potential to be a security risk in some applications. For this reason,
GitHub has deprecated the command and a warning of this is shown in the workflow run summary page of any workflow using
it:

The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more
information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/

The identical capability is now provided in a safer form via the GitHub Actions "environment files" system. Migrating
the use of the deprecated workflow commands to use the `GITHUB_OUTPUT` environment file instead fixes any potential
vulnerabilities in the workflows, resolves the warnings, and avoids the eventual complete breakage of the workflows that
would result from GitHub's planned removal of the `set-output` workflow command 2023-05-31.
2022-11-04 00:45:14 -07:00
dependabot[bot]
b998d35524 Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:39:12 -07:00
dependabot[bot]
ddec64c4a5 Bump svenstaro/upload-release-action from 2.2.0 to 2.3.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.2.0...2.3.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:09:57 -07:00
dependabot[bot]
8fed08003e Bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:09:37 -07:00
dependabot[bot]
8454c625f7 Bump geekyeggo/delete-artifact from 1 to 2
Bumps [geekyeggo/delete-artifact](https://github.com/geekyeggo/delete-artifact) from 1 to 2.
- [Release notes](https://github.com/geekyeggo/delete-artifact/releases)
- [Commits](https://github.com/geekyeggo/delete-artifact/compare/v1...v2)

---
updated-dependencies:
- dependency-name: geekyeggo/delete-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:04:18 -07:00
dependabot[bot]
60df322f09 Bump actions/setup-node from 1 to 3
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 1 to 3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:03:47 -07:00
dependabot[bot]
8bfb140e7c Bump actions/setup-python from 2 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:51:19 -07:00
dependabot[bot]
260227e79a Bump peter-evans/create-pull-request from 3 to 4
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3 to 4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:30:50 -07:00
dependabot[bot]
cc310bf1a5 Bump actions/download-artifact from 2 to 3
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:25:30 -07:00
per1234
dbd52e2f34 Remove unused GitHub release download stats workflow
The "github-stats" GitHub Actions workflow periodically gathers GitHub release asset download statistics for Arduino CLI
and pushes the results to Datadog.

There are no known problems with this workflow. However, the companion "arduino-stats" workflow that did the same for
the downloads of Arduino IDE from downloads.arduino.cc was broken and thus removed from the repository.

The GitHub stats are not very valuable on their own as they only provide an unknown fraction of the total downloads of
Arduino IDE. They have also not ended up being used.

The workflow also uses deprecated Node.js 12 runtime, which currently results in warnings printed to the workflow run
summary page, but will eventually cause the complete breakage of the workflow.

Since it doesn't provide any value and represents a maintenance burden, the workflow is hereby removed from the
repository.
2022-11-03 14:13:40 -07:00
per1234
9cd03bec46 Remove broken download stats workflow
The "arduino-stats" GitHub Actions workflow was designed to periodically gather download statistics from Arduino CDN and
push results to Datadog.

The recorded stats from the identical system in the Arduino CLI repository showed a periodic decrease in total download
count. Since this is patently impossible, it is clear that something is wrong with the system and that the recorded data
is not trustworthy. An investigation into the problem
was never done.

On 2022-03-14, the runs of the "arduino-stats" GitHub Actions workflow began to fail. Because there had not been any
relevant change in the repository between the last successful run and the first failing run, it seems that some external
change caused the breakage.

The workflow also uses deprecated Node.js 12 runtime-based actions and set-output workflow command, which currently
results in warnings printed to the workflow run summary page, but will eventually cause the complete breakage of the
workflow.

Since the workflow was not ever working successfully and the lack of an investigation about that indicates that the
stats are not of immediate importance, the best course of action is to simply remove the broken infrastructure from the
repository rather than investing time into fixing something that isn't being used anyway.
2022-11-03 14:13:40 -07:00
dependabot[bot]
c29452a858 Bump carlosperate/download-file-action from 1 to 2
Bumps [carlosperate/download-file-action](https://github.com/carlosperate/download-file-action) from 1 to 2.
- [Release notes](https://github.com/carlosperate/download-file-action/releases)
- [Commits](https://github.com/carlosperate/download-file-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: carlosperate/download-file-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:05:51 -07:00
per1234
7d91f2d8cb Configure Dependabot to check for outdated actions used in workflows
Dependabot will periodically check the versions of all actions used in the repository's workflows. If any are found to
be outdated, it will submit a pull request to update them.

NOTE: Dependabot's PRs will occasionally propose to pin to the patch version of the action (e.g., updating
`uses: foo/bar@v1` to `uses: foo/bar@v2.3.4`). When the action author has provided a major version ref, use that instead
(e.g., `uses: foo/bar@v2`). Dependabot will automatically close its PR once the workflow has been updated.

More information:
https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
2022-11-03 13:56:30 -07:00
Akos Kitta
f6275f9f62 feat: build IDE2 on darwin arm64
- Use Node.js 16+,
 - All workflow files use `.yml` instead of `.yaml`,
 - Use Arduino LS `0.7.2`,
 - Updated `electron-builder` to `23.3.3`,
 - Removed unused `conf-node-gyp.sh`,
 - Removed unused `THEIA_ELECTRON_SKIP_REPLACE_FFMPEG`, and
 - Aligned `node-gyp@9.3.0`, `electron-rebuild@3.2.9` to Theia.

Co-authored-by: per1234 <accounts@perglass.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-02 15:48:04 +01:00
per1234
0d0550974a Bump version metadata post release
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

In order to cause these builds to be correctly considered "newer" than the release version, the version metadata must be
bumped immediately following each release.

This will also serve as the metadata bump for the next release in the event that release is a minor release. 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-10-28 06:42:46 -07:00
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
318 changed files with 22238 additions and 12546 deletions

View File

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

View File

@@ -30,7 +30,7 @@ body:
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
validations:
required: true
- type: dropdown
@@ -68,7 +68,7 @@ body:
options:
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
- label: I verified the problem still occurs when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
required: true
- label: My report contains all necessary details
required: true

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

@@ -25,7 +25,7 @@ body:
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
validations:
required: true
- type: dropdown
@@ -63,7 +63,7 @@ body:
options:
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
- label: I verified the feature was still missing when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
required: true
- label: My request contains all necessary details
required: true

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# See: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file
version: 2
updates:
# Configure check for outdated GitHub Actions actions in workflows.
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/dependabot/README.md
# See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
- package-ecosystem: github-actions
directory: / # Check the repository's workflows under /.github/workflows/
assignees:
- per1234
schedule:
interval: daily
labels:
- "topic: infrastructure"

View File

@@ -1,131 +0,0 @@
import boto3
import semver
import os
import logging
import uuid
import time
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
log = logging.getLogger()
logging.getLogger("boto3").setLevel(logging.CRITICAL)
logging.getLogger("botocore").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
def execute(client, statement, dest_s3_output_location):
log.info("execute query: {} dumping in {}".format(statement, dest_s3_output_location))
result = client.start_query_execution(
QueryString=statement,
ClientRequestToken=str(uuid.uuid4()),
ResultConfiguration={
"OutputLocation": dest_s3_output_location,
},
)
execution_id = result["QueryExecutionId"]
log.info("wait for query {} completion".format(execution_id))
wait_for_query_execution_completion(client, execution_id)
log.info("operation successful")
return execution_id
def wait_for_query_execution_completion(client, query_execution_id):
query_ended = False
while not query_ended:
query_execution = client.get_query_execution(QueryExecutionId=query_execution_id)
state = query_execution["QueryExecution"]["Status"]["State"]
if state == "SUCCEEDED":
query_ended = True
elif state in ["FAILED", "CANCELLED"]:
raise BaseException(
"query failed or canceled: {}".format(query_execution["QueryExecution"]["Status"]["StateChangeReason"])
)
else:
time.sleep(1)
def valid(key):
split = key.split("_")
if len(split) < 1:
return False
try:
semver.parse(split[0])
except ValueError:
return False
return True
def get_results(client, execution_id):
results_paginator = client.get_paginator("get_query_results")
results_iter = results_paginator.paginate(QueryExecutionId=execution_id, PaginationConfig={"PageSize": 1000})
res = {}
for results_page in results_iter:
for row in results_page["ResultSet"]["Rows"][1:]:
# Loop through the JSON objects
key = row["Data"][0]["VarCharValue"]
if valid(key):
res[key] = row["Data"][1]["VarCharValue"]
return res
def convert_data(data):
result = []
for key, value in data.items():
# 0.18.0_macOS_64bit.tar.gz
split_key = key.split("_")
if len(split_key) != 3:
continue
(version, os_version, arch) = split_key
arch_split = arch.split(".")
if len(arch_split) < 1:
continue
arch = arch_split[0]
if len(arch) > 10:
# This can't be an architecture really.
# It's an ugly solution but works for now so deal with it.
continue
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
result.append(
{
"type": "gauge",
"name": "arduino.downloads.total",
"value": value,
"host": os.environ["GITHUB_REPOSITORY"],
"tags": [
f"version:{version}",
f"os:{os_version}",
f"arch:{arch}",
"cdn:downloads.arduino.cc",
f"project:{repo}",
],
}
)
return result
if __name__ == "__main__":
DEST_S3_OUTPUT = os.environ["AWS_ATHENA_OUTPUT_LOCATION"]
AWS_ATHENA_SOURCE_TABLE = os.environ["AWS_ATHENA_SOURCE_TABLE"]
session = boto3.session.Session(region_name="us-east-1")
athena_client = session.client("athena")
# Load all partitions before querying downloads
execute(athena_client, f"MSCK REPAIR TABLE {AWS_ATHENA_SOURCE_TABLE};", DEST_S3_OUTPUT)
query = f"""SELECT replace(json_extract_scalar(url_decode(url_decode(querystring)),
'$.data.url'), 'https://downloads.arduino.cc/arduino-ide/arduino-ide_', '')
AS flavor, count(json_extract(url_decode(url_decode(querystring)),'$')) AS gauge
FROM {AWS_ATHENA_SOURCE_TABLE}
WHERE json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
LIKE 'https://downloads.arduino.cc/arduino-ide/arduino-ide_%'
AND json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
NOT LIKE '%latest%' -- exclude latest redirect
group by 1 ;"""
exec_id = execute(athena_client, query, DEST_S3_OUTPUT)
results = get_results(athena_client, exec_id)
result_json = convert_data(results)
print(f"::set-output name=result::{result_json}")

View File

@@ -1,57 +0,0 @@
name: arduino-stats
on:
schedule:
# run every day at 07:00 AM, 03:00 PM and 11:00 PM
- cron: "0 7,15,23 * * *"
workflow_dispatch:
repository_dispatch:
jobs:
push-stats:
# This workflow is only of value to the arduino/arduino-ide repository and
# would always fail in forks
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Fetch downloads count form Arduino CDN using AWS Athena
id: fetch
env:
AWS_ACCESS_KEY_ID: ${{ secrets.STATS_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATS_AWS_SECRET_ACCESS_KEY }}
AWS_ATHENA_SOURCE_TABLE: ${{ secrets.STATS_AWS_ATHENA_SOURCE_TABLE }}
AWS_ATHENA_OUTPUT_LOCATION: ${{ secrets.STATS_AWS_ATHENA_OUTPUT_LOCATION }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
pip install boto3 semver
python .github/tools/fetch_athena_stats.py
- name: Send metrics
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
# Metrics input expects YAML but JSON will work just right.
metrics: ${{steps.fetch.outputs.result}}
- name: Report failure
if: failure()
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
events: |
- title: "Arduino IDE stats failing"
text: "Stats collection failed"
alert_type: "error"
host: ${{ github.repository }}
tags:
- "project:arduino-ide"
- "cdn:downloads.arduino.cc"
- "workflow:${{ github.workflow }}"

View File

@@ -55,16 +55,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v1
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Python 3.x
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'
@@ -109,7 +109,7 @@ jobs:
yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: electron/build/dist/build-artifacts/
@@ -140,13 +140,13 @@ jobs:
steps:
- name: Download job transfer artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
- name: Upload tester build artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact.name }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
@@ -158,7 +158,7 @@ jobs:
BODY: ${{ steps.changelog.outputs.BODY }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0 # To fetch all history for all branches and tags.
@@ -180,15 +180,19 @@ jobs:
fi
fi
echo -e "$BODY"
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
# Set workflow step output
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
DELIMITER="$RANDOM"
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "$DELIMITER" >> $GITHUB_OUTPUT
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: CHANGELOG.txt
@@ -199,7 +203,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -220,7 +224,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -228,10 +232,10 @@ jobs:
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.2.0
uses: svenstaro/upload-release-action@2.4.1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
@@ -263,6 +267,6 @@ jobs:
steps:
- name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v1
uses: geekyeggo/delete-artifact@v2
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}

View File

@@ -59,7 +59,9 @@ jobs:
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-noout -passin env:CERTIFICATE_PASSWORD
-legacy \
-noout \
-passin env:CERTIFICATE_PASSWORD
) || (
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
exit 1
@@ -87,6 +89,7 @@ jobs:
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (
@@ -108,7 +111,7 @@ jobs:
echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT
- name: Check if expiration notification period has been reached
id: check-expiration

View File

@@ -27,12 +27,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
@@ -48,6 +48,8 @@ jobs:
- name: Install dependencies
run: yarn
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for errors
run: yarn i18n:check

View File

@@ -8,7 +8,7 @@ on:
env:
CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 14.x
NODE_VERSION: 16.x
jobs:
create-changelog:
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
@@ -27,7 +27,7 @@ jobs:
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Create full changelog
id: full-changelog

View File

@@ -1,96 +0,0 @@
name: github-stats
on:
schedule:
# run every 30 minutes
- cron: "*/30 * * * *"
workflow_dispatch:
repository_dispatch:
jobs:
push-stats:
# This workflow is only of value to the arduino/arduino-ide repository and
# would always fail in forks
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
steps:
- name: Fetch downloads count
id: fetch
uses: actions/github-script@v4
with:
github-token: ${{github.token}}
script: |
let metrics = []
// Get a list of releases
const opts = github.repos.listReleases.endpoint.merge({
...context.repo
})
const releases = await github.paginate(opts)
// Get download stats for every release
for (const rel of releases) {
// Names for assets are like `arduino-ide_2.0.0-beta.12_Linux_64bit.zip`,
// we'll use this later to split the asset file name more easily
const baseName = `arduino-ide_${rel.name}_`
// Get a list of assets for this release
const opts = github.repos.listReleaseAssets.endpoint.merge({
...context.repo,
release_id: rel.id
})
const assets = await github.paginate(opts)
for (const asset of assets) {
// Ignore files that are not arduino-ide packages
if (!asset.name.startsWith(baseName)) {
continue
}
// Strip the base and remove file extension to get `Linux_32bit`
systemArch = asset.name.replace(baseName, "").split(".")[0].split("_")
// Add a metric object to the list of gathered metrics
metrics.push({
"type": "gauge",
"name": "arduino.downloads.total",
"value": asset.download_count,
"host": "${{ github.repository }}",
"tags": [
`version:${rel.name}`,
`os:${systemArch[0]}`,
`arch:${systemArch[1]}`,
"cdn:github.com",
"project:arduino-ide"
]
})
}
}
// The action will put whatever we return from this function in
// `outputs.result`, JSON encoded. So we just return the array
// of objects and GitHub will do the rest.
return metrics
- name: Send metrics
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
# Metrics input expects YAML but JSON will work just right.
metrics: ${{steps.fetch.outputs.result}}
- name: Report failure
if: failure()
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
events: |
- title: "Arduino IDE stats failing"
text: "Stats collection failed"
alert_type: "error"
host: ${{ github.repository }}
tags:
- "project:arduino-ide"
- "cdn:github.com"
- "workflow:${{ github.workflow }}"

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
@@ -45,7 +45,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated translation files
title: Update translation files

View File

@@ -27,11 +27,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download JSON schema for labels configuration file
id: download-schema
uses: carlosperate/download-file-action@v1
uses: carlosperate/download-file-action@v2
with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json
location: ${{ runner.temp }}/label-configuration-schema
@@ -66,12 +66,12 @@ jobs:
steps:
- name: Download
uses: carlosperate/download-file-action@v1
uses: carlosperate/download-file-action@v2
with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
- name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: |
*.yaml
@@ -103,19 +103,19 @@ jobs:
run: |
# Use of this flag in the github-label-sync command will cause it to only check the validity of the
# configuration.
echo "::set-output name=flag::--dry-run"
echo "flag=--dry-run" >> $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download configuration files artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v1
uses: geekyeggo/delete-artifact@v2
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}

View File

@@ -9,7 +9,7 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
NODE_VERSION: 14.x
NODE_VERSION: 16.x
jobs:
pull-from-jsonbin:

8
.gitignore vendored
View File

@@ -4,10 +4,10 @@ node_modules/
lib/
downloads/
build/
Examples/
arduino-ide-extension/Examples/
!electron/build/
src-gen/
!webpack.config.js
webpack.config.js
gen-webpack.config.js
.DS_Store
# switching from `electron` to `browser` in dev mode.
@@ -15,11 +15,11 @@ gen-webpack.config.js
yarn*.log
# For the VS Code extensions used by Theia.
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces
# any Arduino LS generated log files
inols*.log

8
.vscode/launch.json vendored
View File

@@ -14,14 +14,14 @@
".",
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
"--open-devtools"
"--open-devtools",
"--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"
@@ -51,12 +51,12 @@
".",
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339"
"--hosted-plugin-inspect=9339",
"--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"

View File

@@ -2,6 +2,9 @@
"files.exclude": {
"**/lib": false
},
"search.exclude": {
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0",
"version": "2.0.4",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -17,31 +17,36 @@
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w",
"test": "mocha \"./lib/test/**/*.test.js\"",
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@theia/application-package": "1.31.1",
"@theia/core": "1.31.1",
"@theia/debug": "1.31.1",
"@theia/editor": "1.31.1",
"@theia/electron": "1.31.1",
"@theia/filesystem": "1.31.1",
"@theia/keymaps": "1.31.1",
"@theia/markers": "1.31.1",
"@theia/messages": "1.31.1",
"@theia/monaco": "1.31.1",
"@theia/monaco-editor-core": "1.67.2",
"@theia/navigator": "1.31.1",
"@theia/outline-view": "1.31.1",
"@theia/output": "1.31.1",
"@theia/plugin-ext": "1.31.1",
"@theia/preferences": "1.31.1",
"@theia/scm": "1.31.1",
"@theia/search-in-workspace": "1.31.1",
"@theia/terminal": "1.31.1",
"@theia/typehierarchy": "1.31.1",
"@theia/workspace": "1.31.1",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
@@ -50,49 +55,53 @@
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/react-virtualized": "^9.21.21",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.1.0",
"@vscode/debugprotocol": "^1.51.0",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"classnames": "^2.3.1",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2",
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"open": "^8.0.6",
"p-queue": "^5.0.0",
"p-debounce": "^2.1.0",
"p-queue": "^2.4.2",
"ps-tree": "^1.2.0",
"query-string": "^7.0.1",
"react-disable": "^0.1.0",
"react-disable": "^0.1.1",
"react-markdown": "^8.0.0",
"react-select": "^3.0.4",
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2",
"react-virtualized": "^9.22.3",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"url": "^0.11.0",
"which": "^1.3.1"
},
"devDependencies": {
@@ -101,11 +110,10 @@
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
@@ -115,9 +123,6 @@
"moment": "^2.24.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
@@ -158,16 +163,16 @@
],
"arduino": {
"cli": {
"version": "0.27.1"
"version": "0.29.0"
},
"fwuploader": {
"version": "2.2.0"
"version": "2.2.2"
},
"clangd": {
"version": "14.0.0"
},
"languageServer": {
"version": "0.7.1"
"version": "0.7.4"
}
}
}

View File

@@ -42,6 +42,9 @@
const suffix = (() => {
switch (platform) {
case 'darwin':
if (arch === 'arm64') {
return 'macOS_ARM64.tar.gz';
}
return 'macOS_64bit.tar.gz';
case 'win32':
return 'Windows_64bit.zip';

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

@@ -76,6 +76,12 @@
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'darwin-arm64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_ARM64.tar.gz';
clangdSuffix = 'macOS_ARM64';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');

View File

@@ -31,7 +31,7 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { ArduinoPreferences } from './arduino-preferences';
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { ArduinoMenus } from './menu/arduino-menus';
@@ -58,8 +58,8 @@ export class ArduinoFrontendContribution
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(ElectronWindowPreferences)
private readonly electronWindowPreferences: ElectronWindowPreferences;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@@ -78,10 +78,10 @@ export class ArduinoFrontendContribution
}
onStart(app: FrontendApplication): void {
this.arduinoPreferences.onPreferenceChanged((event) => {
this.electronWindowPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'arduino.window.zoomLevel':
case 'window.zoomLevel':
if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
@@ -91,11 +91,10 @@ export class ArduinoFrontendContribution
}
});
this.appStateService.reachedState('ready').then(() =>
this.arduinoPreferences.ready.then(() => {
this.electronWindowPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get(
'arduino.window.zoomLevel'
);
const zoomLevel =
this.electronWindowPreferences.get('window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
})
);

View File

@@ -1,12 +1,9 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import {
TabBarToolbarContribution,
TabBarToolbarFactory,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import {
FrontendApplicationContribution,
@@ -26,7 +23,7 @@ import {
SketchesService,
SketchesServicePath,
} from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -53,8 +50,6 @@ import {
DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import {
@@ -86,10 +81,7 @@ import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import {
MonacoThemeJson,
MonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import {
ArduinoDaemonPath,
ArduinoDaemon,
@@ -135,11 +127,10 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { OpenSettings } from './contributions/open-settings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { BurnBootloader } from './contributions/burn-bootloader';
@@ -183,8 +174,6 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug } from './contributions/debug';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
@@ -207,12 +196,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,
@@ -247,7 +232,6 @@ import { UploadFirmware } from './contributions/upload-firmware';
import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate';
@@ -264,7 +248,6 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c
import {
UserFieldsDialog,
UserFieldsDialogProps,
UserFieldsDialogWidget,
} from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
@@ -277,7 +260,6 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
import {
IDEUpdaterDialog,
IDEUpdaterDialogProps,
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model';
@@ -319,10 +301,6 @@ import { SelectedBoard } from './contributions/selected-board';
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
import { OpenBoardsConfig } from './contributions/open-boards-config';
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
import { MonacoThemeServiceIsReady } from './utils/window';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { StatusBarImpl } from './theia/core/status-bar';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
import { EditorMenuContribution } from './theia/editor/editor-file';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
@@ -337,32 +315,38 @@ import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
{
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/default.color-theme.json'),
},
{
id: 'arduino-theme-dark',
label: 'Dark (Arduino)',
uiTheme: 'vs-dark',
json: require('../../src/browser/data/dark.color-theme.json'),
},
];
themes.forEach((theme) => MonacoThemingService.register(theme));
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = window as any;
const ready = global[MonacoThemeServiceIsReady] as Deferred;
if (ready) {
ready.promise.then(registerArduinoThemes);
} else {
registerArduinoThemes();
}
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';
import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ThemeServiceWithDB } from './theia/core/theming';
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch';
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
import { CreateFeatures } from './create/create-features';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@@ -401,6 +385,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
LibraryListWidgetFrontendContribution
);
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
// Sketch list service
bind(SketchesService)
@@ -423,6 +408,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(ConfigServiceClient).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
// Boards service
bind(BoardsService)
@@ -467,6 +454,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
BoardsListWidgetFrontendContribution
);
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -585,14 +573,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(WorkspaceDeleteHandler)
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -604,9 +584,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 +593,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(
@@ -731,7 +697,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, EditContributions);
Contribution.configure(bind, QuitApp);
Contribution.configure(bind, SketchControl);
Contribution.configure(bind, Settings);
Contribution.configure(bind, OpenSettings);
Contribution.configure(bind, BurnBootloader);
Contribution.configure(bind, BuiltInExamples);
Contribution.configure(bind, LibraryExamples);
@@ -761,7 +727,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch);
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch);
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -846,9 +818,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(
@@ -862,10 +831,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// To avoid running a status bar update on every single `keypress` event from the editor.
bind(StatusBarImpl).toSelf().inSingletonScope();
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
// Debounced update for the tab-bar toolbar when typing in the editor.
bind(DockPanelRenderer).toSelf();
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
@@ -916,6 +881,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
id: 'arduino-sketchbook-widget',
createWidget: () => container.get(SketchbookWidget),
}));
bind(SketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'sketchbook-composite-widget',
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
}));
bind(CloudSketchbookWidget).toSelf();
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
@@ -924,6 +894,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
bind(CreateApi).toSelf().inSingletonScope();
bind(SketchCache).toSelf().inSingletonScope();
bind(CreateFeatures).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFeatures);
bind(ShareSketchDialog).toSelf().inSingletonScope();
bind(AuthenticationClientService).toSelf().inSingletonScope();
@@ -945,12 +917,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
bind(WidgetFactory).toDynamicValue((ctx) => ({
id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
}));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware',
@@ -961,13 +932,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'UploadCertificate',
});
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
bind(IDEUpdaterDialogProps).toConstantValue({
title: 'IDEUpdater',
});
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
@@ -994,4 +963,55 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
// custom window titles
bind(WindowTitleUpdater).toSelf().inSingletonScope();
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
bind(ThemeServiceWithDB).toSelf().inSingletonScope();
rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyServiceProvider).toService(
TypeHierarchyServiceProvider
);
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
// patched the debugger for `cortex-debug@1.5.1`
// https://github.com/eclipse-theia/theia/issues/11871
// https://github.com/eclipse-theia/theia/issues/11879
// https://github.com/eclipse-theia/theia/issues/11880
// https://github.com/eclipse-theia/theia/issues/11885
// https://github.com/eclipse-theia/theia/issues/11886
// https://github.com/eclipse-theia/theia/issues/11916
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
bind(DebugToolbar).toSelf().inSingletonScope();
rebind(TheiaDebugToolbar).toService(DebugToolbar);
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: DebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolbar).toSelf(); // patched toolbar
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf();
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();
});

View File

@@ -114,11 +114,12 @@ export const ArduinoConfigSchema: PreferenceSchema = {
},
'arduino.window.zoomLevel': {
type: 'number',
description: nls.localize(
'arduino/preferences/window.zoomLevel',
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
),
description: '',
default: 0,
deprecationMessage: nls.localize(
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
),
},
'arduino.ide.updateChannel': {
type: 'string',
@@ -249,6 +250,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,
},
},
};
@@ -262,7 +271,6 @@ export interface ArduinoConfiguration {
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
'arduino.board.certificates': string;
@@ -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

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

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,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
AttachedBoardsChangeEvent,
BoardConfig as ProtocolBoardConfig,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
@@ -19,10 +19,7 @@ import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export type Config = ProtocolBoardConfig;
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
@@ -113,11 +110,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 +132,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 +259,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 +302,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 +330,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 +355,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 +387,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

@@ -80,16 +80,16 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
string,
Disposable & { label: string }
>();
let selectedValue = '';
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id };
const selectedValue = value.value;
const handler = {
execute: () =>
this.boardsDataStore.selectConfigOption({
fqbn,
option,
selectedValue,
selectedValue: value.value,
}),
isToggled: () => value.selected,
};
@@ -100,8 +100,14 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
{ label: value.label }
)
);
if (value.selected) {
selectedValue = value.label;
}
}
this.menuRegistry.registerSubmenu(menuPath, label);
this.menuRegistry.registerSubmenu(
menuPath,
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() =>
@@ -111,7 +117,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

@@ -30,11 +30,11 @@ export class BoardsDataStore implements FrontendApplicationContribution {
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>();
protected readonly onChangedEmitter = new Emitter<string[]>();
onStart(): void {
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
let shouldFireChanged = false;
const dataDidChangePerFqbn: string[] = [];
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty)
@@ -49,18 +49,18 @@ export class BoardsDataStore implements FrontendApplicationContribution {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
shouldFireChanged = true;
dataDidChangePerFqbn.push(fqbn);
}
}
}
}
if (shouldFireChanged) {
this.fireChanged();
if (dataDidChangePerFqbn.length) {
this.fireChanged(...dataDidChangePerFqbn);
}
});
}
get onChanged(): Event<void> {
get onChanged(): Event<string[]> {
return this.onChangedEmitter.event;
}
@@ -116,7 +116,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn,
data: { ...data, selectedProgrammer },
});
this.fireChanged();
this.fireChanged(fqbn);
return true;
}
@@ -146,7 +146,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
return false;
}
await this.setData({ fqbn, data });
this.fireChanged();
this.fireChanged(fqbn);
return true;
}
@@ -190,8 +190,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
}
protected fireChanged(): void {
this.onChangedEmitter.fire();
protected fireChanged(...fqbn: string[]): void {
this.onChangedEmitter.fire(fqbn);
}
}

View File

@@ -30,7 +30,6 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
searchable: service,
installable: service,
itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All' },

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

@@ -0,0 +1,102 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { deepClone } from '@theia/core/lib/common/objects';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { ConfigService, ConfigState } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
@injectable()
export class ConfigServiceClient implements FrontendApplicationContribution {
@inject(ConfigService)
private readonly delegate: ConfigService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(MessageService)
private readonly messageService: MessageService;
private readonly didChangeSketchDirUriEmitter = new Emitter<
URI | undefined
>();
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
private readonly toDispose = new DisposableCollection(
this.didChangeSketchDirUriEmitter,
this.didChangeDataDirUriEmitter
);
private config: ConfigState | undefined;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(async () => {
const config = await this.delegate.getConfiguration();
this.use(config);
});
}
onStart(): void {
this.notificationCenter.onConfigDidChange((config) => this.use(config));
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSketchDirUri(): Event<URI | undefined> {
return this.didChangeSketchDirUriEmitter.event;
}
get onDidChangeDataDirUri(): Event<URI | undefined> {
return this.didChangeDataDirUriEmitter.event;
}
/**
* CLI config related error messages if any.
*/
tryGetMessages(): string[] | undefined {
return this.config?.messages;
}
/**
* `directories.user`
*/
tryGetSketchDirUri(): URI | undefined {
return this.config?.config?.sketchDirUri
? new URI(this.config?.config?.sketchDirUri)
: undefined;
}
/**
* `directories.data`
*/
tryGetDataDirUri(): URI | undefined {
return this.config?.config?.dataDirUri
? new URI(this.config?.config?.dataDirUri)
: undefined;
}
private use(config: ConfigState): void {
const oldConfig = deepClone(this.config);
this.config = config;
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
}
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
}
if (this.config.messages?.length) {
const message = this.config.messages.join(' ');
// toast the error later otherwise it might not show up in IDE2
setTimeout(() => this.messageService.error(message), 1_000);
}
}
}

View File

@@ -41,22 +41,16 @@ export class About extends Contribution {
}
async showAbout(): Promise<void> {
const {
version,
commit,
status: cliStatus,
} = await this.configService.getVersion();
const version = await this.configService.getVersion();
const buildDate = this.buildDate;
const detail = (showAll: boolean) =>
nls.localize(
'arduino/about/detail',
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
remote.app.getVersion(),
buildDate ? buildDate : nls.localize('', 'dev build'),
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
version,
cliStatus ? ` ${cliStatus}` : '',
commit,
nls.localize(
'arduino/about/copyright',
'Copyright © {0} Arduino SA',

View File

@@ -7,15 +7,16 @@ import {
CommandRegistry,
MenuModelRegistry,
URI,
Sketch,
} from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
@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 +32,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,13 +42,12 @@ export class AddFile extends SketchContribution {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
modal: true,
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
@@ -79,6 +79,22 @@ export class AddFile extends SketchContribution {
}
);
}
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
private resolveTarget(
sketch: Sketch,
toAddUri: URI
): { uri: URI; filename: string } {
const path = toAddUri.path;
const filename = path.base;
let root = new URI(sketch.uri);
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
root = root.resolve('data');
}
return { uri: root.resolve(filename), filename: filename };
}
}
export namespace AddFile {

View File

@@ -2,7 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
@@ -16,14 +15,11 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected 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 +39,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

@@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
SketchContribution,
@@ -10,7 +9,7 @@ import {
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
@injectable()
export class ArchiveSketch extends SketchContribution {
@@ -28,11 +27,8 @@ export class ArchiveSketch extends SketchContribution {
});
}
protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
private async archiveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
@@ -40,16 +36,19 @@ export class ArchiveSketch extends SketchContribution {
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
const defaultContainerUri = await this.defaultUri();
const defaultUri = defaultContainerUri.resolve(archiveBasename);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
}
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
});
if (!filePath || canceled) {
return;
}
@@ -57,7 +56,7 @@ export class ArchiveSketch extends SketchContribution {
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
await this.sketchesService.archive(sketch, destinationUri.toString());
this.messageService.info(
nls.localize(
'arduino/sketch/createdArchive',

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';
@@ -21,6 +20,7 @@ import {
InstalledBoardWithPackage,
AvailablePorts,
Port,
getBoardInfo,
} from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { nls } from '@theia/core/lib/common';
@@ -50,52 +50,28 @@ export class BoardSelection extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) {
this.messageService.info(
nls.localize(
'arduino/board/selectBoardForInfo',
'Please select a board to obtain board info.'
)
);
const boardInfo = await getBoardInfo(
this.boardsServiceProvider.boardsConfig.selectedPort,
this.boardsService.getState()
);
if (typeof boardInfo === 'string') {
this.messageService.info(boardInfo);
return;
}
if (!selectedBoard.fqbn) {
this.messageService.info(
nls.localize(
'arduino/board/platformMissing',
"The platform for the selected '{0}' board is not installed.",
selectedBoard.name
)
);
return;
}
if (!selectedPort) {
this.messageService.info(
nls.localize(
'arduino/board/selectPortForInfo',
'Please select a port to obtain board info.'
)
);
return;
}
const boardDetails = await this.boardsService.getBoardDetails({
fqbn: selectedBoard.fqbn,
});
if (boardDetails) {
const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name}
const { BN, VID, PID, SN } = boardInfo;
const detail = `
BN: ${BN}
VID: ${VID}
PID: ${PID}`;
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
detail,
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
});
}
PID: ${PID}
SN: ${SN}
`.trim();
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
detail,
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
});
},
});
}
@@ -156,10 +132,7 @@ PID: ${PID}`;
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
@@ -200,14 +173,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 +214,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 +245,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 +261,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 +303,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 +335,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

@@ -20,7 +20,7 @@ import {
URI,
} from './contribution';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch';
/**
@@ -65,7 +65,7 @@ export class Close extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: nls.localize('vscode/editor.contribution/close', 'Close'),
order: '5',
order: '6',
});
}
@@ -185,7 +185,7 @@ export class Close extends SketchContribution {
private async isCurrentSketchTemp(): Promise<false | Sketch> {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
const isTemp = await this.sketchService.isTemp(currentSketch);
const isTemp = await this.sketchesService.isTemp(currentSketch);
if (isTemp) {
return currentSketch;
}

View File

@@ -0,0 +1,121 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CreateApi } from '../create/create-api';
import { CreateFeatures } from '../create/create-features';
import { CreateUri } from '../create/create-uri';
import { Create, isNotFound } from '../create/typings';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { SketchContribution } from './contribution';
export function sketchAlreadyExists(input: string): string {
return nls.localize(
'arduino/cloudSketch/alreadyExists',
"Cloud sketch '{0}' already exists.",
input
);
}
export function sketchNotFound(input: string): string {
return nls.localize(
'arduino/cloudSketch/notFound',
"Could not pull the cloud sketch '{0}'. It does not exist.",
input
);
}
export const synchronizingSketchbook = nls.localize(
'arduino/cloudSketch/synchronizingSketchbook',
'Synchronizing sketchbook...'
);
export function pullingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pulling',
"Synchronizing sketchbook, pulling '{0}'...",
input
);
}
export function pushingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pushing',
"Synchronizing sketchbook, pushing '{0}'...",
input
);
}
@injectable()
export abstract class CloudSketchContribution extends SketchContribution {
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(CreateFeatures)
protected readonly createFeatures: CreateFeatures;
protected async treeModel(): Promise<
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
> {
const { enabled, session } = this.createFeatures;
if (enabled && session) {
const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget);
if (treeModel) {
const root = treeModel.root;
if (CompositeTreeNode.is(root)) {
return treeModel as CloudSketchbookTreeModel & {
root: CompositeTreeNode;
};
}
}
}
return undefined;
}
protected async pull(
sketch: Create.Sketch
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
const treeModel = await this.treeModel();
if (!treeModel) {
return undefined;
}
const id = CreateUri.toUri(sketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find cloud sketchbook tree node with ID: ${id}.`
);
}
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node });
return node;
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(sketchNotFound(sketch.name));
return undefined;
}
throw err;
}
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
for (const treeWidget of widget.getTreeWidgets()) {
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
}
return undefined;
}
}

View File

@@ -12,7 +12,7 @@ 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 { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
@@ -41,10 +41,9 @@ import { SettingsService } from '../dialogs/settings/settings';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
} from '../sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
@@ -61,6 +60,9 @@ 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';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
export {
Command,
@@ -106,6 +108,9 @@ export abstract class Contribution
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
@@ -138,11 +143,11 @@ export abstract class SketchContribution extends Contribution {
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigServiceClient)
protected readonly configService: ConfigServiceClient;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
protected readonly sketchesService: SketchesService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@@ -156,6 +161,9 @@ export abstract class SketchContribution extends Contribution {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
@@ -169,6 +177,25 @@ export abstract class SketchContribution extends Contribution {
}
return override;
}
/**
* Defaults to `directories.user` if defined and not CLI config errors were detected.
* Otherwise, the URI of the user home directory.
*/
protected async defaultUri(): Promise<URI> {
const errors = this.configService.tryGetMessages();
let defaultUri = this.configService.tryGetSketchDirUri();
if (!defaultUri || errors?.length) {
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
}
return defaultUri;
}
protected async defaultPath(): Promise<string> {
const defaultUri = await this.defaultUri();
return this.fileService.fsPath(defaultUri);
}
}
@injectable()

View File

@@ -3,7 +3,12 @@ import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { NotificationCenter } from '../notification-center';
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
import {
Board,
BoardsService,
ExecutableService,
Sketch,
} from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
URI,
@@ -13,12 +18,11 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
@injectable()
export class Debug extends SketchContribution {
@inject(HostedPluginSupport)
@@ -36,9 +40,6 @@ export class Debug extends SketchContribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/
@@ -186,7 +187,7 @@ export class Debug extends SketchContribution {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
@@ -203,7 +204,28 @@ export class Debug extends SketchContribution {
sketchPath,
configPath,
};
return this.commandService.executeCommand('arduino.debug.start', config);
try {
await this.commandService.executeCommand('arduino.debug.start', config);
} catch (err) {
if (await this.isSketchNotVerifiedError(err, sketch)) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const answer = await this.messageService.error(
nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketch.name
),
yes
);
if (answer === yes) {
this.commandService.executeCommand('arduino-verify-sketch');
}
} else {
this.messageService.error(
err instanceof Error ? err.message : String(err)
);
}
}
}
get compileForDebug(): boolean {
@@ -215,7 +237,24 @@ export class Debug extends SketchContribution {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
this.mainMenuManager.update();
this.menuManager.update();
}
private async isSketchNotVerifiedError(
err: unknown,
sketch: Sketch
): Promise<boolean> {
if (err instanceof Error) {
try {
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return tempBuildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
return false;
}
}
return false;
}
}
export namespace Debug {

View File

@@ -1,32 +1,131 @@
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybeArray } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
SketchContribution,
Sketch,
} from './contribution';
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
import { Sketch } from '../contributions/contribution';
import { isNotFound } from '../create/typings';
import { Command, CommandRegistry } from './contribution';
import { CloudSketchContribution } from './cloud-contribution';
export interface DeleteSketchParams {
/**
* Either the URI of the sketch folder or the sketch to delete.
*/
readonly toDelete: string | Sketch;
/**
* If `true`, the currently opened sketch is expected to be deleted.
* Hence, the editors must be closed, the sketch will be scheduled
* for deletion, and the browser window will close or navigate away.
* If `false`, the sketch will be scheduled for deletion,
* but the current window remains open. If `force`, the window will
* navigate away, but IDE2 won't open any confirmation dialogs.
*/
readonly willNavigateAway?: boolean | 'force';
}
@injectable()
export class DeleteSketch extends SketchContribution {
export class DeleteSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (uri: string) => this.deleteSketch(uri),
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
});
}
private async deleteSketch(uri: string): Promise<void> {
const sketch = await this.loadSketch(uri);
if (!sketch) {
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
private async deleteSketch(params: DeleteSketchParams): Promise<void> {
const { toDelete, willNavigateAway } = params;
let sketch: Sketch;
if (typeof toDelete === 'string') {
const resolvedSketch = await this.loadSketch(toDelete);
if (!resolvedSketch) {
console.info(
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
);
return;
}
sketch = resolvedSketch;
} else {
sketch = toDelete;
}
if (!willNavigateAway) {
this.scheduleDeletion(sketch);
return;
}
return this.sketchService.deleteSketch(sketch);
const cloudUri = this.createFeatures.cloudUri(sketch);
if (willNavigateAway !== 'force') {
const { response } = await remote.dialog.showMessageBox({
title: nls.localizeByDefault('Delete'),
type: 'question',
buttons: [Dialog.CANCEL, Dialog.OK],
message: cloudUri
? nls.localize(
'theia/workspace/deleteCloudSketch',
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
)
: nls.localize(
'theia/workspace/deleteCurrentSketch',
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
),
});
// cancel
if (response === 0) {
return;
}
}
if (cloudUri) {
const posixPath = cloudUri.path.toString();
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
if (!cloudSketch) {
throw new Error(
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
);
}
try {
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
await this.createApi.deleteSketch(cloudSketch.path);
} catch (err) {
if (!isNotFound(err)) {
throw err;
} else {
console.info(
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
);
}
}
}
await Promise.all([
...Sketch.uris(sketch).map((uri) =>
this.closeWithoutSaving(new URI(uri))
),
]);
this.windowService.setSafeToShutDown();
this.scheduleDeletion(sketch);
return window.close();
}
private scheduleDeletion(sketch: Sketch): void {
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
}
private async loadSketch(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.loadSketch(uri);
const sketch = await this.sketchesService.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
@@ -35,6 +134,13 @@ export class DeleteSketch extends SketchContribution {
throw err;
}
}
// fix: https://github.com/eclipse-theia/theia/issues/12107
private async closeWithoutSaving(uri: URI): Promise<void> {
const affected = getAffected(this.shell.widgets, uri);
const toClose = [...affected].map(([, widget]) => widget);
await this.shell.closeMany(toClose, { save: false });
}
}
export namespace DeleteSketch {
export namespace Commands {
@@ -43,3 +149,20 @@ export namespace DeleteSketch {
};
}
}
function getAffected<T extends Widget>(
widgets: Iterable<T>,
context: MaybeArray<URI>
): [URI, T & NavigatableWidget][] {
const uris = Array.isArray(context) ? context : [context];
const result: [URI, T & NavigatableWidget][] = [];
for (const widget of widgets) {
if (NavigatableWidget.is(widget)) {
const resourceUri = widget.getResourceUri();
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
result.push([resourceUri, widget]);
}
}
}
return result;
}

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') }
@@ -81,9 +57,11 @@ export class EditContributions extends Contribution {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`\`\`\`cpp
this.clipboardService.writeText(`
\`\`\`cpp
${value}
\`\`\``);
\`\`\`
`);
}
},
});
@@ -147,23 +125,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 +181,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 +267,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

@@ -12,36 +12,45 @@ import {
} from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service';
import {
SketchContribution,
CommandRegistry,
MenuModelRegistry,
URI,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
import {
Board,
SketchRef,
SketchContainer,
SketchesError,
Sketch,
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { unregisterSubmenu } from '../menu/arduino-menus';
@injectable()
export abstract class Examples extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly toDispose = new DisposableCollection();
protected override init(): void {
@@ -49,12 +58,24 @@ export abstract class Examples extends SketchContribution {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
this.notificationCenter.onDidReinitialize(() =>
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
// No force refresh. The core client was already refreshed.
})
);
}
// 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.
@@ -109,6 +130,11 @@ export abstract class Examples extends SketchContribution {
const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
this.toDispose.push(
Disposable.create(() =>
unregisterSubmenu(submenuPath, this.menuRegistry)
)
);
sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children);
} else {
@@ -150,26 +176,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)
.then((result) => {
const name = new URI(uri).path.base;
this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await
return result;
});
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.sketchesService.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();
@@ -200,30 +254,32 @@ export class BuiltInExamples extends Examples {
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
protected 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

@@ -17,7 +17,7 @@ import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
this.notificationCenter.onLibraryDidUninstall(() =>
this.updateMenuActions()
);
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
}
override async onReady(): Promise<void> {

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

@@ -1,15 +1,20 @@
import { Mutex } from 'async-mutex';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Mutex } from 'async-mutex';
import {
ArduinoDaemon,
assertSanitizedFqbn,
BoardsService,
ExecutableService,
sanitizeFqbn,
} from '../../common/protocol';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { SketchContribution, URI } from './contribution';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { SketchContribution, URI } from './contribution';
import { BoardsDataStore } from '../boards/boards-data-store';
@injectable()
export class InoLanguage extends SketchContribution {
@@ -28,8 +33,15 @@ export class InoLanguage extends SketchContribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex();
private languageServerFqbn?: string;
private languageServerStartMutex = new Mutex();
override onReady(): void {
const start = (
@@ -43,27 +55,61 @@ export class InoLanguage extends SketchContribution {
}
}
};
this.boardsServiceProvider.onBoardsConfigChanged(start);
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig)
);
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
);
this.preferences.onPreferenceChanged(
({ preferenceName, oldValue, newValue }) => {
if (oldValue !== newValue) {
switch (preferenceName) {
case 'arduino.language.log':
case 'arduino.language.realTimeDiagnostics':
start(this.boardsServiceProvider.boardsConfig, true);
const forceRestart = () => {
start(this.boardsServiceProvider.boardsConfig, true);
};
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigChanged(start),
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig)
),
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
),
this.preferences.onPreferenceChanged(
({ preferenceName, oldValue, newValue }) => {
if (oldValue !== newValue) {
switch (preferenceName) {
case 'arduino.language.log':
case 'arduino.language.realTimeDiagnostics':
forceRestart();
}
}
}
}
);
),
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onChanged((dataChangePerFqbn) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const matchingFqbn = dataChangePerFqbn.find(
(fqbn) => sanitizedFqbn === fqbn
);
const { boardsConfig } = this.boardsServiceProvider;
if (
matchingFqbn &&
boardsConfig.selectedBoard?.fqbn === matchingFqbn
) {
start(boardsConfig);
}
}
}),
]);
start(this.boardsServiceProvider.boardsConfig);
}
onStop(): void {
this.toDispose.dispose();
}
private async startLanguageServer(
fqbn: string,
name: string | undefined,
@@ -101,11 +147,18 @@ export class InoLanguage extends SketchContribution {
}
return;
}
if (!forceStart && fqbn === this.languageServerFqbn) {
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
const log = this.preferences.get('arduino.language.log');
const realTimeDiagnostics = this.preferences.get(
'arduino.language.realTimeDiagnostics'
@@ -141,7 +194,7 @@ export class InoLanguage extends SketchContribution {
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn,
fqbn: fqbnWithConfig,
name: name ? `"${name}"` : undefined,
},
realTimeDiagnostics,
@@ -150,7 +203,7 @@ export class InoLanguage extends SketchContribution {
),
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();

View File

@@ -0,0 +1,173 @@
import { injectable } from '@theia/core/shared/inversify';
import {
Contribution,
Command,
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings';
import debounce = require('lodash.debounce');
@injectable()
export class InterfaceScale extends Contribution {
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 {
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
}
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.menuManager.update();
}
}
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

@@ -0,0 +1,165 @@
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
TaskFactoryImpl,
WorkspaceInputDialogWithProgress,
} from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { Command, CommandRegistry, Sketch } from './contribution';
import {
CloudSketchContribution,
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
@injectable()
export class NewCloudSketch extends CloudSketchContribution {
private readonly toDispose = new DisposableCollection();
override onReady(): void {
this.toDispose.pushAll([
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()),
this.createFeatures.onDidChangeSession(() => this.menuManager.update()),
]);
if (this.createFeatures.session) {
this.menuManager.update();
}
}
onStop(): void {
this.toDispose.dispose();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(true),
isEnabled: () => Boolean(this.createFeatures.session),
isVisible: () => this.createFeatures.enabled,
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'),
order: '1',
});
}
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
keybinding: 'CtrlCmd+Alt+N',
});
}
private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
): Promise<void> {
const treeModel = await this.treeModel();
if (treeModel) {
const rootNode = treeModel.root;
return this.openWizard(
rootNode,
treeModel,
skipShowErrorMessageOnOpen,
initialValue
);
}
}
private async openWizard(
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
): Promise<void> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.createNewSketchWithProgress(treeModel, value)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
{
title: nls.localize(
'arduino/newCloudSketch/newSketchTitle',
'Name of the new Cloud Sketch'
),
parentUri: CreateUri.root,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return sketchAlreadyExists(input);
}
return Sketch.validateCloudSketchFolderName(input) ?? '';
},
},
this.labelProvider,
taskFactory
);
await dialog.open(skipShowErrorMessageOnOpen);
if (dialog.taskResult) {
this.openInNewWindow(dialog.taskResult);
}
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(false, taskFactory.value ?? initialValue);
}
throw err;
}
}
private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
return async (progress: Progress) => {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating cloud sketch '{0}'...",
value
),
});
const sketch = await this.createApi.createSketch(value);
progress.report({ message: synchronizingSketchbook });
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
return node;
};
}
private openInNewWindow(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<void> {
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
);
}
}
export namespace NewCloudSketch {
export namespace Commands {
export const NEW_CLOUD_SKETCH: Command = {
id: 'arduino-new-cloud-sketch',
};
}
}

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,17 +16,12 @@ 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 {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New'),
label: nls.localize('arduino/sketch/new', 'New Sketch'),
order: '0',
});
}
@@ -41,7 +35,7 @@ export class NewSketch extends SketchContribution {
async newSketch(): Promise<void> {
try {
const sketch = await this.sketchService.createNewSketch();
const sketch = await this.sketchesService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
await this.messageService.error(e.toString());
@@ -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,7 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import { ExampleRef } from '../../common/protocol';
import { SketchesError } from '../../common/protocol';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@@ -34,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 }) =>
@@ -43,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
}
override async onReady(): Promise<void> {
this.sketchService
.recentlyOpenedSketches()
this.update();
}
private update(forceUpdate?: boolean): void {
this.sketchesService
.recentlyOpenedSketches(forceUpdate)
.then((sketches) => this.refreshMenu(sketches));
}
@@ -56,29 +60,31 @@ export class OpenRecentSketch extends SketchContribution {
);
}
private refreshMenu(sketches: (Sketch | ExampleRef)[]): void {
private refreshMenu(sketches: Sketch[]): void {
this.register(sketches);
this.mainMenuManager.update();
}
protected register(sketches: (Sketch | ExampleRef)[]): void {
protected register(sketches: Sketch[]): void {
const order = 0;
this.toDispose.dispose();
for (const sketch of sketches) {
const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const { uri } = sketch;
const command = { id: `arduino-open-recent--${uri}` };
const handler = {
execute: async () => {
const toOpen = Sketch.is(sketch)
? sketch
: await this.sketchService.cloneExample(sketch.sourceUri);
this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
toOpen
);
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);
@@ -90,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
order: String(order),
}
);
this.toDisposeBeforeRegister.set(
uri,
this.toDispose.pushAll([
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
@@ -99,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
),
]);
}
}
}

View File

@@ -1,32 +1,34 @@
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { Settings } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Command,
MenuModelRegistry,
CommandRegistry,
SketchContribution,
KeybindingRegistry,
MenuModelRegistry,
SketchContribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { nls } from '@theia/core/lib/common';
@injectable()
export class Settings extends SketchContribution {
export class OpenSettings extends SketchContribution {
@inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog;
private readonly settingsDialog: SettingsDialog;
protected settingsOpened = false;
private settingsOpened = false;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, {
registry.registerCommand(OpenSettings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
let settings: Settings | undefined = undefined;
try {
this.settingsOpened = true;
this.menuManager.update();
settings = await this.settingsDialog.open();
} finally {
this.settingsOpened = false;
this.menuManager.update();
}
if (settings) {
await this.settingsService.update(settings);
@@ -41,7 +43,7 @@ export class Settings extends SketchContribution {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id,
commandId: OpenSettings.Commands.OPEN.id,
label:
nls.localize(
'vscode/preferences.contribution/preferences',
@@ -57,13 +59,13 @@ export class Settings extends SketchContribution {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
command: OpenSettings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
});
}
}
export namespace Settings {
export namespace OpenSettings {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-settings-open',

View File

@@ -2,7 +2,7 @@ import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { Later } from '../../common/nls';
import { SketchesError } from '../../common/protocol';
import { Sketch, SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
@@ -10,12 +10,18 @@ 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';
@injectable()
export class OpenSketchFiles extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI) => this.openSketchFiles(uri),
execute: (uri: URI, focusMainSketchFile) =>
this.openSketchFiles(uri, focusMainSketchFile),
});
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
execute: (
@@ -28,13 +34,19 @@ export class OpenSketchFiles extends SketchContribution {
});
}
private async openSketchFiles(uri: URI): Promise<void> {
private async openSketchFiles(
uri: URI,
focusMainSketchFile = false
): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const sketch = await this.sketchesService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
if (focusMainSketchFile) {
await this.ensureOpened(mainFileUri, true, { mode: 'activate' });
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(
'arduino/common/oldFormat',
@@ -55,9 +67,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,8 +99,33 @@ 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(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService,
sketchesService: this.sketchesService,
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();
const sketch = await this.sketchesService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
}
@@ -80,20 +133,84 @@ export class OpenSketchFiles extends SketchContribution {
uri: string,
forceOpen = false,
options?: EditorOpenerOptions
): Promise<unknown> {
): Promise<EditorWidget | undefined> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(
if (widget && !forceOpen) {
return widget;
}
const disposables = new DisposableCollection();
const deferred = new Deferred<EditorWidget>();
// An editor can be in two primary states:
// - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event.
// - The editor is opened. Can be active, current, or open.
// - If the editor has the focus (the cursor blinks in the editor): it's the active editor.
// - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor.
// - If the editor is not the top editor in the main area, it's opened.
if (!widget) {
// If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event
// and resolve the promise with the editor only when the new editor's visibility changes.
disposables.push(
this.editorManager.onCreated((editor) => {
if (editor.editor.uri.toString() === uri) {
if (editor.isAttached && editor.isVisible) {
deferred.resolve(editor);
} else {
disposables.push(
editor.onDidChangeVisibility((visible) => {
if (visible) {
// wait an animation frame. although the visible and attached props are true the editor is not there.
// let the browser render the widget
setTimeout(
() =>
requestAnimationFrame(() => deferred.resolve(editor)),
0
);
}
})
);
}
}
})
);
}
this.editorManager
.open(
new URI(uri),
options ?? {
mode: 'reveal',
preview: false,
counter: 0,
}
)
.then((editorWidget) => {
// If the widget was defined, it was already opened.
// The editor is expected to be attached to the shell and visible in the UI.
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
// It can resolve earlier.
if (widget) {
deferred.resolve(editorWidget);
}
});
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
const result: EditorWidget | undefined | 'timeout' = await Promise.race([
deferred.promise,
wait(timeout).then(() => {
disposables.dispose();
return 'timeout' as const;
}),
]);
if (result === 'timeout') {
console.warn(
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
);
return undefined;
}
return result;
}
}
export namespace OpenSketchFiles {

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);
}
},
});
@@ -119,7 +54,7 @@ export class OpenSketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
order: '1',
order: '2',
});
}
@@ -130,30 +65,37 @@ 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.sketchesService.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> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
private async selectSketch(): Promise<Sketch | undefined> {
const defaultPath = await this.defaultPath();
const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
}
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
});
if (!filePaths.length) {
return undefined;
}
@@ -164,50 +106,16 @@ export class OpenSketch extends SketchContribution {
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
if (sketch) {
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,
sketchesService: this.sketchesService,
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 +125,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;
sketchesService: SketchesService;
labelProvider: LabelProvider;
}
): Promise<Sketch | undefined> {
const { fileService, sketchesService, 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 sketchesService.getSketchFolder(newSketchUri.toString());
}
}

View File

@@ -0,0 +1,166 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import {
TaskFactoryImpl,
WorkspaceInputDialogWithProgress,
} from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import {
CloudSketchContribution,
pullingSketch,
pushingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch, URI } from './contribution';
export interface RenameCloudSketchParams {
readonly cloudUri: URI;
readonly sketch: Sketch;
}
@injectable()
export class RenameCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
execute: (params: RenameCloudSketchParams) =>
this.renameSketch(params, true),
});
}
private async renameSketch(
params: RenameCloudSketchParams,
skipShowErrorMessageOnOpen: boolean,
initValue: string = params.sketch.name
): Promise<string | undefined> {
const treeModel = await this.treeModel();
if (treeModel) {
const posixPath = params.cloudUri.path.toString();
const node = treeModel.getNode(posixPath);
const parentNode = node?.parent;
if (
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
CompositeTreeNode.is(parentNode)
) {
return this.openWizard(
params,
node,
parentNode,
treeModel,
skipShowErrorMessageOnOpen,
initValue
);
}
}
return undefined;
}
private async openWizard(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
parentNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
): Promise<string | undefined> {
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
? parentNode.uri
: CreateUri.root;
const existingNames = parentNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.renameSketchWithProgress(params, node, treeModel, value)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
{
title: nls.localize(
'arduino/renameCloudSketch/renameSketchTitle',
'New name of the Cloud Sketch'
),
parentUri,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return sketchAlreadyExists(input);
}
return Sketch.validateCloudSketchFolderName(input) ?? '';
},
},
this.labelProvider,
taskFactory
);
await dialog.open(skipShowErrorMessageOnOpen);
return dialog.taskResult;
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.renameSketch(
params,
false,
taskFactory.value ?? initialValue
);
}
throw err;
}
}
private renameSketchWithProgress(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
treeModel: CloudSketchbookTreeModel,
value: string
): (progress: Progress) => Promise<string | undefined> {
return async (progress: Progress) => {
const fromName = params.cloudUri.path.name;
const fromPosixPath = params.cloudUri.path.toString();
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node);
// rename
progress.report({
message: nls.localize(
'arduino/cloudSketch/renaming',
"Renaming cloud sketch from '{0}' to '{1}'...",
fromName,
value
),
});
await this.createApi.rename(fromPosixPath, toPosixPath);
// sync
progress.report({
message: synchronizingSketchbook,
});
this.createApi.sketchCache.init(); // invalidate the cache
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
if (!sketch) {
return undefined;
}
await treeModel.refresh();
// pull
progress.report({ message: pullingSketch(sketch.name) });
const pulledNode = await this.pull(sketch);
return pulledNode
? node.uri.parent.resolve(sketch.name).toString()
: undefined;
};
}
}
export namespace RenameCloudSketch {
export namespace Commands {
export const RENAME_CLOUD_SKETCH: Command = {
id: 'arduino-rename-cloud-sketch',
};
}
}

View File

@@ -1,28 +1,34 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTask } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import {
SketchContribution,
URI,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
URI,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import { StartupTask } from '../../electron-common/startup-task';
import { DeleteSketch } from './delete-sketch';
import {
RenameCloudSketch,
RenameCloudSketchParams,
} from './rename-cloud-sketch';
@injectable()
export class SaveAsSketch extends SketchContribution {
export class SaveAsSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
@@ -35,7 +41,7 @@ export class SaveAsSketch extends SketchContribution {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
label: nls.localizeByDefault('Save As...'),
order: '7',
});
}
@@ -50,7 +56,7 @@ export class SaveAsSketch extends SketchContribution {
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
private async saveAs(
{
execOnlyIfTemp,
openAfterMove,
@@ -63,101 +69,230 @@ export class SaveAsSketch extends SketchContribution {
return false;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return false;
let destinationUri: string | undefined;
const cloudUri = this.createFeatures.cloudUri(sketch);
if (cloudUri) {
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
} else {
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
}
// 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(
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,
});
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return false;
}
const workspaceUri = await this.sketchService.copy(sketch, {
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
destinationUri,
});
if (workspaceUri) {
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
if (markAsRecentlyOpened) {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
if (!newWorkspaceUri) {
return false;
}
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
if (openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
args: [{ toDelete: sketch.uri }],
});
}
this.workspaceService.open(new URI(workspaceUri), options);
this.workspaceService.open(new URI(newWorkspaceUri), options);
}
return !!workspaceUri;
return !!newWorkspaceUri;
}
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
private async createCloudCopy(
params: RenameCloudSketchParams
): Promise<string | undefined> {
return this.commandService.executeCommand<string>(
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
params
);
}
private async createLocalCopy(
sketch: Sketch,
execOnlyIfTemp?: boolean
): Promise<string | undefined> {
const isTemp = await this.sketchesService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return undefined;
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = await this.defaultUri();
// 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}
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
const defaultUri = containerDirUri.resolve(
Sketch.toValidSketchFolderName(sketch.name, exists)
);
const defaultPath = await this.fileService.fsPath(defaultUri);
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
}
/**
* Prompts for the new sketch folder name until a valid one is give,
* then resolves with the destination sketch folder URI string,
* or `undefined` if the operation was canceled.
*/
private async promptLocalSketchFolderDestination(
sketch: Sketch,
defaultPath: string
): Promise<string | undefined> {
let sketchFolderDestinationUri: string | undefined;
while (!sketchFolderDestinationUri) {
const { filePath } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath) {
return undefined;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
// The new location of the sketch cannot be inside the location of current sketch.
// https://github.com/arduino/arduino-ide/issues/1882
let dialogContent: InvalidSketchFolderDialogContent | undefined;
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
dialogContent = {
message: nls.localize(
'arduino/sketch/invalidSketchFolderLocationMessage',
"Invalid sketch folder location: '{0}'",
filePath
),
details: nls.localize(
'arduino/sketch/invalidSketchFolderLocationDetails',
'You cannot save a sketch into a folder inside itself.'
),
question: nls.localize(
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
'Do you want to try saving the sketch to a different location?'
),
};
}
if (!dialogContent) {
const sketchFolderName = new URI(destinationUri).path.base;
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
if (errorMessage) {
dialogContent = {
message: nls.localize(
'arduino/sketch/invalidSketchFolderNameMessage',
"Invalid sketch folder name: '{0}'",
sketchFolderName
),
details: errorMessage,
question: nls.localize(
'arduino/sketch/editInvalidSketchFolderQuestion',
'Do you want to try saving the sketch with a different name?'
),
};
}
}
if (dialogContent) {
const message = `
${dialogContent.message}
${dialogContent.details}
${dialogContent.question}`.trim();
defaultPath = filePath;
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message,
buttons: [Dialog.CANCEL, Dialog.YES],
}
);
// cancel
if (response === 0) {
return undefined;
}
} else {
sketchFolderDestinationUri = destinationUri;
}
}
return sketchFolderDestinationUri;
}
private async saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string
): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, object>();
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
const uriString = uri?.toString();
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
if (
uriString.includes(sketch.uri) &&
saveable &&
saveable.createSnapshot
) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (mainFileUri === uriString) {
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketchUri.length);
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
} catch (e) {
console.error(e);
}
}));
})
);
}
}
interface InvalidSketchFolderDialogContent {
readonly message: string;
readonly details: string;
readonly question: string;
}
export namespace SaveAsSketch {
export namespace Commands {
export const SAVE_AS_SKETCH: Command = {

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,
@@ -11,7 +10,7 @@ import {
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
@@ -19,19 +18,13 @@ 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 {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'),
order: '6',
order: '7',
});
}
@@ -47,7 +40,7 @@ export class SaveSketch extends SketchContribution {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const isTemp = await this.sketchesService.isTemp(sketch);
if (isTemp) {
return this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
@@ -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

@@ -1,50 +1,34 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import {
URI,
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
MenuModelRegistry,
open,
SketchContribution,
TabBarToolbarRegistry,
URI,
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@injectable()
export class SketchControl extends SketchContribution {
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
private readonly shell: ApplicationShell;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
private readonly contextMenuRenderer: ContextMenuRenderer;
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
@@ -57,107 +41,57 @@ export class SketchControl extends SketchContribution {
this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
let parentElement: HTMLElement | undefined = undefined;
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
);
if (target instanceof HTMLElement) {
parentElement = target.parentElement ?? undefined;
}
if (!parentElement) {
return;
}
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_RENAME.id,
label: nls.localize('vscode/fileActions/rename', 'Rename'),
order: '1',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_RENAME
)
)
);
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_DELETE.id,
label: nls.localize('vscode/fileActions/delete', 'Delete'),
order: '2',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_DELETE
)
)
);
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowRename(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_RENAME.id,
label: nls.localize('vscode/fileActions/rename', 'Rename'),
order: '1',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_RENAME
)
)
);
} else {
const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/rename', 'Rename')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
)
);
}
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowDelete(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: nls.localize('vscode/fileActions/delete', 'Delete'),
order: '2',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_DELETE
)
)
);
} else {
const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/delete', 'Delete')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
)
);
}
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
@@ -176,7 +110,7 @@ export class SketchControl extends SketchContribution {
{
commandId: command.id,
label: this.labelProvider.getName(uri),
order: `${i}`,
order: String(i).padStart(4),
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
@@ -193,6 +127,7 @@ export class SketchControl extends SketchContribution {
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
showDisabled: true,
};
this.contextMenuRenderer.render(options);
},
@@ -235,7 +170,7 @@ export class SketchControl extends SketchContribution {
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
keybinding: 'CtrlCmd+Alt+Left',
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
@@ -249,27 +184,6 @@ export class SketchControl extends SketchContribution {
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected isCloudSketch(uri: string): boolean {
try {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
} catch {
return false;
}
}
protected allowRename(uri: string): boolean {
return !this.isCloudSketch(uri);
}
protected allowDelete(uri: string): boolean {
return !this.isCloudSketch(uri);
}
}
export namespace SketchControl {

View File

@@ -3,7 +3,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@@ -38,7 +38,7 @@ export class SketchFilesTracker extends SketchContribution {
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchService.loadSketch(
const reloadedSketch = await this.sketchesService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {

View File

@@ -1,44 +1,27 @@
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());
this.configService.onDidChangeSketchDirUri(() => this.update());
}
override async onReady(): Promise<void> {
this.update();
}
private update() {
this.sketchService.getSketches({}).then((container) => {
protected override update(): void {
this.sketchesService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
this.menuManager.update();
});
}
@@ -50,7 +33,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 +45,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

@@ -12,7 +12,6 @@ import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import {
arduinoCert,
certificateList,
@@ -31,22 +30,29 @@ export class UploadCertificate extends Contribution {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false;
override onStart(): void {
this.preferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.board.certificates') {
this.menuManager.update();
}
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open();
} finally {
this.dialogOpened = false;
this.menuManager.update();
}
},
isEnabled: () => !this.dialogOpened,
@@ -54,7 +60,7 @@ export class UploadCertificate extends Contribution {
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => {
const certs = this.arduinoPreferences.get('arduino.board.certificates');
const certs = this.preferences.get('arduino.board.certificates');
this.preferenceService.set(
'arduino.board.certificates',
@@ -75,7 +81,6 @@ export class UploadCertificate extends Contribution {
.join(' ')}`
);
},
isEnabled: () => true,
});
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
@@ -89,7 +94,6 @@ export class UploadCertificate extends Contribution {
args: [args.cert],
});
},
isEnabled: () => true,
});
}

View File

@@ -21,9 +21,11 @@ export class UploadFirmware extends Contribution {
execute: async () => {
try {
this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open();
} finally {
this.dialogOpened = false;
this.menuManager.update();
}
},
isEnabled: () => !this.dialogOpened,

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, sanitizeFqbn } 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 { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../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 {
@@ -191,6 +106,7 @@ export class UploadSketch extends CoreServiceContribution {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.menuManager.update();
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire();
this.clearVisibleNotification();
@@ -215,18 +131,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,9 +147,11 @@ export class UploadSketch extends CoreServiceContribution {
{ timeout: 3000 }
);
} catch (e) {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.menuManager.update();
this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire();
}
@@ -258,12 +165,12 @@ 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([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
@@ -300,23 +207,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.
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
}
}
export namespace UploadSketch {
@@ -328,7 +218,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,126 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { 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 } 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();
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.menuManager.update();
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: 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

@@ -0,0 +1,202 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import { Sketch, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
@injectable()
export class ValidateSketch extends CloudSketchContribution {
override onReady(): void {
this.validate();
}
private async validate(): Promise<void> {
const result = await this.promptFixActions();
if (!result) {
const yes = await this.prompt(
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
nls.localize(
'arduino/validateSketch/abortFixMessage',
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
Dialog.NO
),
[Dialog.NO, Dialog.YES]
);
if (yes) {
return this.validate();
}
const sketch = await this.sketchesService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), {
preserveWindow: true,
});
}
}
/**
* Returns with an array of actions the user has to perform to fix the invalid sketch.
*/
private validateSketch(
sketch: Sketch,
dataDirUri: URI | undefined
): FixAction[] {
// sketch code file validation errors first as they do not require window reload
const actions = Sketch.uris(sketch)
.filter((uri) => uri !== sketch.mainFileUri)
.map((uri) => new URI(uri))
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
.map((uri) => ({
uri,
error: this.doValidate(sketch, dataDirUri, uri.path.name),
}))
.filter(({ error }) => Boolean(error))
.map((object) => <{ uri: URI; error: string }>object)
.map(({ uri, error }) => ({
execute: async () => {
const unknown =
(await this.promptRenameSketchFile(uri, error)) &&
(await this.commandService.executeCommand(
WorkspaceCommands.FILE_RENAME.id,
uri
));
return !!unknown;
},
}));
// sketch folder + main sketch file last as it requires a `Save as...` and the window reload
const sketchFolderName = new URI(sketch.uri).path.base;
const sketchFolderNameError = this.doValidate(
sketch,
dataDirUri,
sketchFolderName
);
if (sketchFolderNameError) {
actions.push({
execute: async () => {
const unknown =
(await this.promptRenameSketch(sketch, sketchFolderNameError)) &&
(await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
<SaveAsSketch.Options>{
markAsRecentlyOpened: true,
openAfterMove: true,
wipeOriginal: true,
}
));
return !!unknown;
},
});
}
return actions;
}
private doValidate(
sketch: Sketch,
dataDirUri: URI | undefined,
toValidate: string
): string | undefined {
const cloudUri = this.createFeatures.isCloud(sketch, dataDirUri);
return cloudUri
? Sketch.validateCloudSketchFolderName(toValidate)
: Sketch.validateSketchFolderName(toValidate);
}
private async currentSketch(): Promise<Sketch> {
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
if (CurrentSketch.isValid(sketch)) {
return sketch;
}
const deferred = new Deferred<Sketch>();
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
(sketch) => {
if (CurrentSketch.isValid(sketch)) {
disposable.dispose();
deferred.resolve(sketch);
}
}
);
return deferred.promise;
}
private async promptFixActions(): Promise<boolean> {
const maybeDataDirUri = this.configService.tryGetDataDirUri();
const [sketch, dataDirUri] = await Promise.all([
this.currentSketch(),
maybeDataDirUri ??
waitForEvent(this.configService.onDidChangeDataDirUri, 5_000),
]);
const fixActions = this.validateSketch(sketch, dataDirUri);
for (const fixAction of fixActions) {
const result = await fixAction.execute();
if (!result) {
return false;
}
}
return true;
}
private async promptRenameSketch(
sketch: Sketch,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFolderTitle',
'Invalid sketch name'
),
nls.localize(
'arduino/validateSketch/renameSketchFolderMessage',
"The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
sketch.name,
error
)
);
}
private async promptRenameSketchFile(
uri: URI,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFileTitle',
'Invalid sketch filename'
),
nls.localize(
'arduino/validateSketch/renameSketchFileMessage',
"The sketch file '{0}' cannot be used. {1} Do you want to rename the sketch file now?",
uri.path.base,
error
)
);
}
private async prompt(
title: string,
message: string,
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
): Promise<boolean> {
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
title,
message,
type: 'warning',
buttons,
}
);
// cancel
if (response === 0) {
return false;
}
return true;
}
}
interface FixAction {
execute(): Promise<boolean>;
}

View File

@@ -11,7 +11,7 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler';
@@ -21,11 +21,18 @@ export interface VerifySketchParams {
*/
readonly exportBinaries?: boolean;
/**
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
*/
readonly silent?: boolean;
}
/**
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
*/
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
@injectable()
export class VerifySketch extends CoreServiceContribution {
@inject(CoreErrorHandler)
@@ -33,22 +40,24 @@ export class VerifySketch extends CoreServiceContribution {
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private verifyInProgress = false;
private verifyProgress: VerifyProgress = 'idle';
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: (params?: VerifySketchParams) => this.verifySketch(params),
isEnabled: () => !this.verifyInProgress,
isEnabled: () => this.verifyProgress === 'idle',
});
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch({ exportBinaries: true }),
isEnabled: () => !this.verifyInProgress,
isEnabled: () => this.verifyProgress === 'idle',
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.verifyInProgress,
isToggled: () => this.verifyInProgress,
isEnabled: () => this.verifyProgress !== 'explicit-verify',
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit-verify',
execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
});
@@ -99,15 +108,16 @@ export class VerifySketch extends CoreServiceContribution {
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
if (this.verifyInProgress) {
if (this.verifyProgress !== 'idle') {
return undefined;
}
try {
if (!params?.silent) {
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
}
this.verifyProgress = params?.silent
? 'automatic-verify'
: 'explicit-verify';
this.onDidChangeEmitter.fire();
this.menuManager.update();
this.clearVisibleNotification();
this.coreErrorHandler.reset();
@@ -139,10 +149,9 @@ export class VerifySketch extends CoreServiceContribution {
this.handleError(e);
return undefined;
} finally {
this.verifyInProgress = false;
if (!params?.silent) {
this.onDidChangeEmitter.fire();
}
this.verifyProgress = 'idle';
this.onDidChangeEmitter.fire();
this.menuManager.update();
}
}

View File

@@ -1,12 +1,16 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { fetch } from 'cross-fetch';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import { Create, CreateError } from './typings';
export interface ResponseResultProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(response: Response): Promise<any>;
}
export namespace ResponseResultProvider {
@@ -15,6 +19,8 @@ export namespace ResponseResultProvider {
export const JSON: ResponseResultProvider = (response) => response.json();
}
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
export function Utf8ArrayToStr(array: Uint8Array): string {
let out, i, c;
let char2, char3;
@@ -61,20 +67,13 @@ type ResourceType = 'f' | 'd';
@injectable()
export class CreateApi {
@inject(SketchCache)
protected sketchCache: SketchCache;
protected authenticationService: AuthenticationClientService;
protected arduinoPreferences: ArduinoPreferences;
public init(
authenticationService: AuthenticationClientService,
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this;
}
readonly sketchCache: SketchCache;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesService)
private readonly sketchesService: SketchesService;
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
return {
@@ -129,10 +128,13 @@ export class CreateApi {
async createSketch(
posixPath: string,
content: string = CreateApi.defaultInoContent
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent()
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const headers = await this.headers();
const [headers, content] = await Promise.all([
this.headers(),
contentProvider,
]);
const payload = {
ino: btoa(content),
path: posixPath,
@@ -291,7 +293,7 @@ export class CreateApi {
this.sketchCache.addSketch(sketch);
let file = '';
if (sketch && sketch.secrets) {
if (sketch.secrets) {
for (const item of sketch.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`;
}
@@ -381,7 +383,7 @@ export class CreateApi {
return;
}
// do not upload "do_not_sync" files/directoris and their descendants
// do not upload "do_not_sync" files/directories and their descendants
const segments = posixPath.split(posix.sep) || [];
if (
segments.some((segment) => Create.do_not_sync_files.includes(segment))
@@ -415,6 +417,21 @@ export class CreateApi {
await this.delete(posixPath, 'd');
}
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
* instead of 404 when deleting a non-existing resource.
*/
async deleteSketch(sketchPath: string): Promise<void> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
await this.run(url, {
method: 'DELETE',
headers,
});
}
private async delete(posixPath: string, type: ResourceType): Promise<void> {
const url = new URL(
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
@@ -475,14 +492,12 @@ export class CreateApi {
}
private async run<T>(
requestInfo: RequestInfo | URL,
requestInfo: URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> {
const response = await fetch(
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
init
);
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`);
const response = await fetch(requestInfo.toString(), init);
if (!response.ok) {
let details: string | undefined = undefined;
try {
@@ -516,19 +531,3 @@ export class CreateApi {
return this.authenticationService.session?.accessToken || '';
}
}
export namespace CreateApi {
export const defaultInoContent = `/*
*/
void setup() {
}
void loop() {
}
`;
}

View File

@@ -0,0 +1,95 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Sketch } from '../../common/protocol';
import { AuthenticationSession } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
@injectable()
export class CreateFeatures implements FrontendApplicationContribution {
@inject(ArduinoPreferences)
private readonly preferences: ArduinoPreferences;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
private readonly onDidChangeSessionEmitter = new Emitter<
AuthenticationSession | undefined
>();
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeSessionEmitter,
this.onDidChangeEnabledEmitter
);
private _enabled: boolean;
private _session: AuthenticationSession | undefined;
onStart(): void {
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange((session) => {
const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.onDidChangeSessionEmitter.fire(this._session);
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.onDidChangeEnabledEmitter.fire(this._enabled);
}
}
}),
]);
this._enabled = this.preferences['arduino.cloud.enabled'];
this._session = this.authenticationService.session;
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
return this.onDidChangeSessionEmitter.event;
}
get onDidChangeEnabled(): Event<boolean> {
return this.onDidChangeEnabledEmitter.event;
}
get enabled(): boolean {
return this._enabled;
}
get session(): AuthenticationSession | undefined {
return this._session;
}
/**
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
* Returns with `undefined` if `dataDirUri` is `undefined`.
*/
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
if (!dataDirUri) {
console.warn(
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
);
return undefined;
}
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
}
cloudUri(sketch: Sketch): URI | undefined {
if (!this.session) {
return undefined;
}
return this.localCacheFsProvider.from(new URI(sketch.uri));
}
}

View File

@@ -189,10 +189,6 @@ export class CreateFsProvider
FileSystemProviderErrorCode.NoPermissions
);
}
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
return this.createApi;
}
}

View File

@@ -1,4 +1,4 @@
import { URI as Uri } from 'vscode-uri';
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { toPosixPath, parentPosix, posix } from './create-paths';
import { Create } from './typings';
@@ -7,7 +7,9 @@ export namespace CreateUri {
export const scheme = 'arduino-create';
export const root = toUri(posix.sep);
export function toUri(posixPathOrResource: string | Create.Resource): URI {
export function toUri(
posixPathOrResource: string | Create.Resource | Create.Sketch
): URI {
const posixPath =
typeof posixPathOrResource === 'string'
? posixPathOrResource

View File

@@ -71,3 +71,23 @@ export class CreateError extends Error {
Object.setPrototypeOf(this, CreateError.prototype);
}
}
export type ConflictError = CreateError & { status: 409 };
export function isConflict(err: unknown): err is ConflictError {
return isErrorWithStatusOf(err, 409);
}
export type NotFoundError = CreateError & { status: 404 };
export function isNotFound(err: unknown): err is NotFoundError {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is CreateError & { status: number } {
if (err instanceof CreateError) {
return err.status === status;
}
return false;
}

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#dae3e3",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#00818480",
"editorCursor.foreground": "#434f54",
"editorCursor.foreground": "#dae3e3",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#171e21",
"editorWidget.foreground": "#dae3e3",
@@ -67,7 +67,8 @@
"tree.indentGuidesStroke": "#374146",
"tab.unfocusedActiveForeground": "#dae3e3",
"tab.inactiveBackground": "#171e21",
"textLink.foreground": "#0ca1a6"
"textLink.foreground": "#0ca1a6",
"errorForeground": "#df7365"
},
"tokenColors": [
{

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#4e5b61",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#7fcbcdb3",
"editorCursor.foreground": "#434f54",
"editorCursor.foreground": "#4e5b61",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#f7f9f9",
"editorWidget.foreground": "#4e5b61",
@@ -67,7 +67,8 @@
"tree.indentGuidesStroke": "#dae3e3",
"tab.unfocusedActiveForeground": "#4e5b61",
"tab.inactiveBackground": "#ecf1f1",
"textLink.foreground": "#008184"
"textLink.foreground": "#008184",
"errorForeground": "#df7365"
},
"tokenColors": [
{

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

@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from 'electron';
import { clipboard } from '@theia/core/electron-shared/@electron/remote';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { CreateApi } from '../create/create-api';

View File

@@ -5,10 +5,8 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { ReactDialog } from '../../theia/dialogs/dialogs';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
@@ -23,26 +21,30 @@ import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends ReactDialog<void> {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
private updatableFqbns: string[] = [];
private availableBoards: AvailableBoard[] = [];
private isOpen = new Object();
private busy = false;
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
constructor(
@inject(UploadFirmwareDialogProps)
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
@postConstruct()
@@ -59,77 +61,34 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
});
}
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return (
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
);
}
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private busy = false;
constructor(
@inject(UploadFirmwareDialogProps)
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
</div>
);
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
const firstButton = this.node.querySelector('button');
firstButton?.focus();
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
@@ -138,11 +97,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
if (this.busy) {
return;
}
this.widget.close();
super.close();
this.isOpen = new Object();
}
busyCallback(busy: boolean): void {
private busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
@@ -150,4 +109,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
this.closeCrossNode.classList.remove('disabled');
}
}
private flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
}

View File

@@ -1,7 +1,6 @@
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';
@@ -28,32 +27,19 @@ export const IDEUpdaterComponent = ({
},
}: IDEUpdaterComponentProps): React.ReactElement => {
const { version, releaseNotes } = updateInfo;
const changelogDivRef =
React.useRef() as React.MutableRefObject<HTMLDivElement>;
const [changelog, setChangelog] = React.useState<string>('');
React.useEffect(() => {
if (!!releaseNotes && changelogDivRef.current) {
let changelog: string;
if (typeof releaseNotes === 'string') changelog = releaseNotes;
else
changelog = releaseNotes.reduce((acc, item) => {
return item.note ? (acc += `${item.note}\n\n`) : acc;
}, '');
ReactDOM.render(
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a onClick={() => href && shell.openExternal(href)} {...props}>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>,
changelogDivRef.current
if (releaseNotes) {
setChangelog(
typeof releaseNotes === 'string'
? releaseNotes
: releaseNotes.reduce(
(acc, item) => (item.note ? (acc += `${item.note}\n\n`) : acc),
''
)
);
}
}, [updateInfo]);
}, [releaseNotes, changelog]);
const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded">
@@ -106,9 +92,24 @@ export const IDEUpdaterComponent = ({
version
)}
</div>
{releaseNotes && (
{changelog && (
<div className="dialogRow changelog-container">
<div className="changelog" ref={changelogDivRef} />
<div className="changelog">
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a
onClick={() => href && shell.openExternal(href)}
{...props}
>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>
</div>
</div>
)}
</div>

View File

@@ -5,10 +5,8 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { ReactDialog } from '../../theia/dialogs/dialogs';
import { nls } from '@theia/core';
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
import {
@@ -22,47 +20,11 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {
private _updateInfo: UpdateInfo;
private _updateProgress: UpdateProgress = {};
setUpdateInfo(updateInfo: UpdateInfo): void {
this._updateInfo = updateInfo;
this.update();
}
mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
get updateInfo(): UpdateInfo {
return this._updateInfo;
}
get updateProgress(): UpdateProgress {
return this._updateProgress;
}
protected render(): React.ReactNode {
return !!this._updateInfo ? (
<IDEUpdaterComponent
updateInfo={this._updateInfo}
updateProgress={this._updateProgress}
/>
) : null;
}
}
@injectable()
export class IDEUpdaterDialogProps extends DialogProps {}
@injectable()
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(IDEUpdaterDialogWidget)
private readonly widget: IDEUpdaterDialogWidget;
export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@@ -75,6 +37,9 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(WindowService)
private readonly windowService: WindowService;
private _updateInfo: UpdateInfo | undefined;
private _updateProgress: UpdateProgress = {};
constructor(
@inject(IDEUpdaterDialogProps)
protected override readonly props: IDEUpdaterDialogProps
@@ -94,26 +59,34 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
protected init(): void {
this.updaterClient.onUpdaterDidFail((error) => {
this.appendErrorButtons();
this.widget.mergeUpdateProgress({ error });
this.mergeUpdateProgress({ error });
});
this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
this.widget.mergeUpdateProgress({ progressInfo });
this.mergeUpdateProgress({ progressInfo });
});
this.updaterClient.onDownloadDidFinish(() => {
this.appendInstallButtons();
this.widget.mergeUpdateProgress({ downloadFinished: true });
this.mergeUpdateProgress({ downloadFinished: true });
});
}
get value(): UpdateInfo {
return this.widget.updateInfo;
protected render(): React.ReactNode {
return (
this.updateInfo && (
<IDEUpdaterComponent
updateInfo={this.updateInfo}
updateProgress={this.updateProgress}
/>
)
);
}
get value(): UpdateInfo | undefined {
return this.updateInfo;
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.update();
this.appendInitialButtons();
super.onAfterAttach(msg);
}
@@ -196,15 +169,19 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
}
private skipVersion(): void {
if (!this.updateInfo) {
console.warn(`Nothing to skip. No update info is available`);
return;
}
this.localStorageService.setData<string>(
SKIP_IDE_VERSION,
this.widget.updateInfo.version
this.updateInfo.version
);
this.close();
}
private startDownload(): void {
this.widget.mergeUpdateProgress({
this.mergeUpdateProgress({
downloadStarted: true,
});
this.clearButtons();
@@ -216,31 +193,48 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
this.close();
}
private set updateInfo(updateInfo: UpdateInfo | undefined) {
this._updateInfo = updateInfo;
this.update();
}
private get updateInfo(): UpdateInfo | undefined {
return this._updateInfo;
}
private get updateProgress(): UpdateProgress {
return this._updateProgress;
}
private mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
this.widget.mergeUpdateProgress({
this.mergeUpdateProgress({
progressInfo: undefined,
downloadStarted: false,
downloadFinished: false,
error: undefined,
});
this.widget.setUpdateInfo(data);
this.updateInfo = data;
return super.open();
}
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
this.update();
}
override close(): void {
this.widget.dispose();
if (
this.widget.updateProgress?.downloadStarted &&
!this.widget.updateProgress?.downloadFinished
this.updateProgress?.downloadStarted &&
!this.updateProgress?.downloadFinished
) {
this.updater.stopDownload();
}

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,29 +200,32 @@ 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>
<div className="flex-line">
<select
className="theia-select"
value={ThemeService.get().getCurrentTheme().label}
value={this.props.themeService.getCurrentTheme().label}
onChange={this.themeDidChange}
>
{ThemeService.get()
.getThemes()
.map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
{this.props.themeService.getThemes().map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
</select>
</div>
<div className="flex-line">
@@ -260,7 +273,7 @@ export class SettingsComponent extends React.Component<
>
{CompilerWarningLiterals.map((value) => (
<option key={value} value={value}>
{value}
{CompilerWarnings.labelOf(value)}
</option>
))}
</select>
@@ -393,15 +406,27 @@ export class SettingsComponent extends React.Component<
}
onChange={this.socksProtocolDidChange}
/>
SOCKS
SOCKS5
</label>
</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 +527,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 +566,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 });
};
@@ -585,11 +610,11 @@ export class SettingsComponent extends React.Component<
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
const theme = this.props.themeService.getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
if (ThemeService.get().getCurrentTheme().id !== theme.id) {
ThemeService.get().setCurrentTheme(theme.id);
if (this.props.themeService.getCurrentTheme().id !== theme.id) {
this.props.themeService.setCurrentTheme(theme.id);
}
}
};
@@ -657,7 +682,7 @@ export class SettingsComponent extends React.Component<
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks';
network.protocol = event.target.checked ? 'http' : 'socks5';
this.setState({ network });
}
};
@@ -667,7 +692,7 @@ export class SettingsComponent extends React.Component<
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks' : 'http';
network.protocol = event.target.checked ? 'socks5' : 'http';
this.setState({ network });
}
};
@@ -728,6 +753,7 @@ export namespace SettingsComponent {
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
readonly localizationProvider: AsyncLocalizationProvider;
readonly themeService: ThemeService;
}
export type State = Settings & {
rawAdditionalUrlsValue: string;

View File

@@ -35,6 +35,9 @@ export class SettingsWidget extends ReactWidget {
@inject(AsyncLocalizationProvider)
protected readonly localizationProvider: AsyncLocalizationProvider;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected render(): React.ReactNode {
return (
<SettingsComponent
@@ -43,6 +46,7 @@ export class SettingsWidget extends ReactWidget {
fileDialogService={this.fileDialogService}
windowService={this.windowService}
localizationProvider={this.localizationProvider}
themeService={this.themeService}
/>
);
}
@@ -59,6 +63,9 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
@inject(ThemeService)
private readonly themeService: ThemeService;
constructor(
@inject(SettingsDialogProps)
protected override readonly props: SettingsDialogProps
@@ -121,11 +128,11 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
}
override async open(): Promise<Promise<Settings> | undefined> {
const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id;
const themeIdBeforeOpen = this.themeService.getCurrentTheme().id;
const result = await super.open();
if (!result) {
if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) {
ThemeService.get().setCurrentTheme(themeIdBeforeOpen);
if (this.themeService.getCurrentTheme().id !== themeIdBeforeOpen) {
this.themeService.setCurrentTheme(themeIdBeforeOpen);
}
}
return result;
@@ -155,7 +162,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 +187,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

@@ -5,7 +5,7 @@ import {
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
@@ -25,17 +25,21 @@ import {
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { DefaultTheme } from '@theia/application-package/lib/application-props';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import type { FileStat } from '@theia/filesystem/lib/common/files';
export const WINDOW_SETTING = 'window';
export const EDITOR_SETTING = 'editor';
export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`;
export const AUTO_SAVE_SETTING = `files.autoSave`;
export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`;
export const ARDUINO_SETTING = 'arduino';
export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
export const ARDUINO_WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`;
export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`;
export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`;
export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`;
export const AUTO_SCALE_SETTING = `${ARDUINO_WINDOW_SETTING}.autoScale`;
export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`;
export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`;
export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`;
@@ -53,7 +57,7 @@ export interface Settings {
currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
interfaceScale: number; // `window.zoomLevel`
verboseOnCompile: boolean; // `arduino.compile.verbose`
compilerWarnings: CompilerWarnings; // `arduino.compile.warnings`
verboseOnUpload: boolean; // `arduino.upload.verbose`
@@ -101,6 +105,9 @@ export class SettingsService {
@inject(CommandService)
protected commandService: CommandService;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
@@ -141,10 +148,9 @@ export class SettingsService {
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
this.preferenceService.get<string>(
'workbench.colorTheme',
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'arduino-theme-dark'
: 'arduino-theme'
DefaultTheme.defaultForOSTheme(
FrontendApplicationConfigProvider.get().defaultTheme
)
),
this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING,
@@ -166,7 +172,15 @@ export class SettingsService {
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
this.configService.getConfiguration(),
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
const {
config = {
additionalUrls: [],
sketchDirUri: '',
network: Network.Default(),
},
} = cliConfig;
const { additionalUrls, sketchDirUri, network } = config;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
@@ -218,7 +232,11 @@ export class SettingsService {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
let sketchbookStat: FileStat | undefined = undefined;
try {
sketchbookStat = await this.fileService.resolve(new URI(sketchbookDir));
} catch {}
if (!sketchbookStat || !sketchbookStat.isDirectory) {
return nls.localize(
'arduino/preferences/invalid.sketchbook.location',
'Invalid sketchbook location: {0}',
@@ -231,11 +249,7 @@ export class SettingsService {
'Invalid editor font size. It must be a positive integer.'
);
}
if (
!ThemeService.get()
.getThemes()
.find(({ id }) => id === themeId)
) {
if (!this.themeService.getThemes().find(({ id }) => id === themeId)) {
return nls.localize(
'arduino/preferences/invalid.theme',
'Invalid theme.'
@@ -252,7 +266,6 @@ export class SettingsService {
private async savePreference(name: string, value: unknown): Promise<void> {
await this.preferenceService.set(name, value, PreferenceScope.User);
await timeout(5);
}
async save(): Promise<string | true> {
@@ -274,28 +287,38 @@ export class SettingsService {
network,
sketchbookShowAllFiles,
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
const [cliConfig, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath),
]);
const { config } = cliConfig;
if (!config) {
// Do not check for any error messages. The config might has errors (such as invalid directories.user) right before saving the new values.
return nls.localize(
'arduino/preferences/noCliConfig',
'Could not load the CLI configuration'
);
}
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
(config as any).network = network;
(config as any).locale = currentLanguage;
await this.savePreference('editor.fontSize', editorFontSize);
await this.savePreference('workbench.colorTheme', themeId);
await this.savePreference(AUTO_SAVE_SETTING, autoSave);
await this.savePreference('editor.quickSuggestions', quickSuggestions);
await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface);
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
await this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile);
await this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings);
await this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload);
await this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload);
await this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles);
await this.configService.setConfiguration(config);
await Promise.all([
this.savePreference('editor.fontSize', editorFontSize),
this.savePreference('workbench.colorTheme', themeId),
this.savePreference(AUTO_SAVE_SETTING, autoSave),
this.savePreference('editor.quickSuggestions', quickSuggestions),
this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface),
this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale),
this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile),
this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings),
this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload),
this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload),
this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles),
this.configService.setConfiguration(config),
]);
this.onDidChangeEmitter.fire(this._settings);
// after saving all the settings, if we need to change the language we need to perform a reload

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

@@ -1,63 +1,18 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
protected _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
}
set currentUserFields(userFields: BoardUserField[]) {
this.setUserFields(userFields);
}
get currentUserFields(): BoardUserField[] {
return this._currentUserFields;
}
resetUserFieldsValue(): void {
this._currentUserFields = this._currentUserFields.map((field) => {
field.value = '';
return field;
});
}
protected setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}
protected render(): React.ReactNode {
return (
<form>
<UserFieldsComponent
initialBoardUserFields={this._currentUserFields}
updateUserFields={this.setUserFields.bind(this)}
cancel={this.cancel}
accept={this.accept}
/>
</form>
);
}
}
import { ReactDialog } from '../../theia/dialogs/dialogs';
@injectable()
export class UserFieldsDialogProps extends DialogProps {}
@injectable()
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
protected readonly widget: UserFieldsDialogWidget;
export class UserFieldsDialog extends ReactDialog<BoardUserField[]> {
private _currentUserFields: BoardUserField[] = [];
constructor(
@inject(UserFieldsDialogProps)
@@ -69,39 +24,36 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
this.titleNode.classList.add('user-fields-dialog-title');
this.contentNode.classList.add('user-fields-dialog-content');
this.acceptButton = undefined;
this.widget = new UserFieldsDialogWidget(
this.close.bind(this),
this.accept.bind(this)
);
}
set value(userFields: BoardUserField[]) {
this.widget.currentUserFields = userFields;
}
get value(): BoardUserField[] {
return this.widget.currentUserFields;
return this._currentUserFields;
}
set value(userFields: BoardUserField[]) {
this._currentUserFields = userFields;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<UserFieldsComponent
initialBoardUserFields={this.value}
updateUserFields={this.doUpdateUserFields}
cancel={this.doCancel}
accept={this.doAccept}
/>
</form>
</div>
);
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override async accept(): Promise<void> {
// If the user presses enter and at least
// a field is empty don't accept the input
@@ -114,8 +66,21 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
}
override close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
this.resetUserFieldsValue();
super.close();
}
private resetUserFieldsValue(): void {
this.value = this.value.map((field) => {
field.value = '';
return field;
});
}
private readonly doCancel: () => void = () => this.close();
private readonly doAccept: () => Promise<void> = () => this.accept();
private readonly doUpdateUserFields: (userFields: BoardUserField[]) => void =
(userFields: BoardUserField[]) => {
this.value = userFields;
};
}

View File

@@ -0,0 +1,6 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/dark/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#C5C5C5" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,6 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/light/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#424242" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -41,7 +41,6 @@ export class LibraryListWidget extends ListWidget<
searchable: service,
installable: service,
itemLabel: (item: LibraryPackage) => item.name,
itemDeprecated: (item: LibraryPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
@@ -119,20 +118,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 +135,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 +196,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

@@ -1,5 +1,5 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from 'vscode-uri';
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
@@ -34,7 +34,6 @@ export class LocalCacheFsProvider
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>();
private _localCacheRoot: URI;
@@ -89,8 +88,25 @@ export class LocalCacheFsProvider
}
protected async init(fileService: FileService): Promise<void> {
const config = await this.configService.getConfiguration();
this._localCacheRoot = new URI(config.dataDirUri);
const { config } = await this.configService.getConfiguration();
// Any possible CLI config errors are ignored here. IDE2 does not verify the `directories.data` folder.
// If the data dir is accessible, IDE2 creates the cache folder for the cloud sketches. Otherwise, it does not.
// The data folder can be configured outside of the IDE2, and the new data folder will be picked up with a
// subsequent IDE2 start.
if (!config?.dataDirUri) {
return; // the deferred promise will never resolve
}
const localCacheUri = new URI(config.dataDirUri);
try {
await fileService.access(localCacheUri);
} catch (err) {
console.error(
`'directories.data' location is inaccessible at ${config.dataDirUri}`,
err
);
return;
}
this._localCacheRoot = localCacheUri;
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot);
@@ -153,7 +169,7 @@ export class LocalCacheFsProvider
return uri;
}
private toUri(session: AuthenticationSession): URI {
toUri(session: AuthenticationSession): URI {
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
return this._localCacheRoot.resolve(session.id.split('|')[1]);
}

View File

@@ -97,6 +97,11 @@ export namespace ArduinoMenus {
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// `Tool` > `Ports` (always visible https://github.com/arduino/arduino-ide/issues/655)
export const TOOLS__PORTS_SUBMENU = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
// -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc.

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';
@@ -15,7 +18,7 @@ import {
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
ConfigState,
Sketch,
ProgressMessage,
} from '../common/protocol';
@@ -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 didReinitializeEmitter = new Emitter<void>();
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<{
config: Config | undefined;
}>();
protected readonly platformDidInstallEmitter = 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<ConfigState>();
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 | 'zip-install';
}>();
private readonly libraryDidUninstallEmitter = new Emitter<{
item: LibraryPackage;
}>();
protected 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.didReinitializeEmitter,
this.indexUpdateWillStartEmitter,
this.indexUpdateDidProgressEmitter,
this.indexDidUpdateEmitter,
this.indexUpdateDidCompleteEmitter,
this.indexUpdateDidFailEmitter,
this.daemonDidStartEmitter,
this.daemonDidStopEmitter,
@@ -82,8 +85,9 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter
);
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
readonly onDidReinitialize = this.didReinitializeEmitter.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 +116,24 @@ export class NotificationCenter
this.toDispose.dispose();
}
notifyIndexWillUpdate(progressId: string): void {
this.indexWillUpdateEmitter.fire(progressId);
notifyDidReinitialize(): void {
this.didReinitializeEmitter.fire();
}
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 {
@@ -142,7 +144,7 @@ export class NotificationCenter
this.daemonDidStopEmitter.fire();
}
notifyConfigDidChange(event: { config: Config | undefined }): void {
notifyConfigDidChange(event: ConfigState): void {
this.configDidChangeEmitter.fire(event);
}
@@ -154,7 +156,9 @@ export class NotificationCenter
this.platformDidUninstallEmitter.fire(event);
}
notifyLibraryDidInstall(event: { item: LibraryPackage }): void {
notifyLibraryDidInstall(event: {
item: LibraryPackage | 'zip-install';
}): void {
this.libraryDidInstallEmitter.fire(event);
}

View File

@@ -1,6 +1,5 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { OptionsType } from 'react-select/src/types';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
@@ -128,9 +127,7 @@ export class MonitorWidget extends ReactWidget {
);
};
protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorModel.EOL>[] {
return [
{
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),

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"
)
);
}
}

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