Compare commits

...

173 Commits

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

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

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

* Scaffold interfaces and classes for pluggable monitors

* Implement MonitorService to handle pluggable monitor lifetime

* Rename WebSocketService to WebSocketProvider and uninjected it

* Moved some interfaces

* Changed upload settings

* Enhance MonitorManager APIs

* Fixed WebSocketChange event signature

* Add monitor proxy functions for the frontend

* Moved settings to MonitorService

* Remove several unnecessary serial monitor classes

* Changed how connection is handled on upload

* Proxied more monitor methods to frontend

* WebSocketProvider is not injectable anymore

* Add generic monitor settings storaging

* More serial classes removal

* Remove unused file

* Changed plotter contribution to use new manager proxy

* Changed MonitorWidget and children to use new monitor proxy

* Updated MonitorWidget to use new monitor proxy

* Fix backend logger bindings

* Delete unnecessary Symbol

* coreClientProvider is now set when constructing MonitorService

* Add missing binding

* Fix `MonitorManagerProxy` DI issue

* fix monitor connection

* delete duplex when connection is closed

* update arduino-cli to 0.22.0

* fix upload when monitor is open

* add MonitorSettingsProvider interface

* monitor settings provider stub

* updated pseudo code

* refactor monitor settings interfaces

* monitor service provider singleton

* add unit tests

* change MonitorService providers to injectable deps

* fix monitor settings client communication

* refactor monitor commands protocol

* use monitor settings provider properly

* add settings to monitor model

* add settings to monitor model

* reset serial monitor when port changes

* fix serial plotter opening

* refine monitor connection settings

* fix hanging web socket connections

* add serial plotter reset command

* send port to web socket clients

* monitor service wait for success serial port open

* fix reset loop

* update serial plotter version

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

* remove useless plotter protocol file

* localize web socket errors

* clean-up code

* update translation file

* Fix duplicated editor tabs (#1012)

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

* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

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

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

* Fixed the translations.

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

* Fixed the translations again.

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

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

* Aligned the stop handler code to Theia.

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

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

* fix serial monitor send line ending

* refactor monitor-service poll for test/readability

* localize web socket errors

* update translation file

* Fix duplicated editor tabs (#1012)

* i18n:check rerun

* Speed up IDE startup time.

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

* override coreClientProvider in monitor-service

* cleanup merged code

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

* 944: Prevent race conditions setting authOptions

* typo correction, duplicate identifier

* prevent block of auth client service on setOptions

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

Closes #1009
Closes #566

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

* Align `CLOSE` command to rest of app

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

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

* Fixed the translations.

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

* Fixed the translations again.

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

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

* Aligned the stop handler code to Theia.

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

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

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

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

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

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

Those issues are resolved by the following changes:

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

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

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

* register localization contribution to backend module

* copy i18n folder to build

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

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

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

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

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

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

The behavior of the NSIS installer is unchanged:

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

Closes #919.

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

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

Closes #881.

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

* rephrase notes for Windows contributor in BUILDING.md

* Update notes for Windows contributor in BUILDING.md

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

* move Notes for Windows contributors in Build from source section

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

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

- bug report
- feature request

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

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

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

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

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

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

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

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

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

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

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

* fix how the old changelog is cut off
2022-03-14 12:05:53 +01:00
Alberto Iannaccone
8cac0872a4 Version 2.0.0-rc5 (#897) 2022-03-10 12:02:13 +01:00
Alberto Iannaccone
70f1c5f8ec Add privacy policy menu item (#883) 2022-03-09 11:46:22 +01:00
ulemons
b416e5f9e8 handling pagination in getting the sketches (#875)
Co-authored-by: Umberto Sgueglia <umberto.sgueglia@external.fcagroup.com>
2022-03-08 17:11:18 +01:00
per1234
bfe6835cab Remove irrelevant statement from EULA
When using the interactive installer, the user is presented with a dialog requested they agree to this.

The previous statement about initiation of a download constituting agreement is relevant in the context of the text's source on the arduino.cc downloads page, but not at all in the context of the installer dialog.
2022-03-08 07:54:12 -08:00
Alberto Iannaccone
9e89964df2 remove dev tools menu item (#882) 2022-03-08 07:38:00 +00:00
Alberto Iannaccone
04c3d0c1d3 Fix sketch name duplicates (#887) 2022-03-07 16:34:16 +00:00
per1234
c9996df11c Add Linux AppImage to nightly build download links
Linux x86-64 builds of the Arduino IDE are now available in AppImage format in additional to the ZIP format.

Since only the AppImage format IDE supports auto-updates (the IDE installed from the ZIP will notify of available updates, but can't auto-update), this will be the preferred format and so good beta testing coverage of it is especially important.
2022-03-07 03:30:31 -08:00
per1234
49971ada07 Remove irrelevant trigger from "Compose full changelog" workflow
The "Compose full changelog" GitHub Actions workflow generates a changelog file from the release notes and uploads this
to Arduino's server for display to the user by the IDE updater.

Previously, this workflow could be triggered by either of two events:

- Release creation
- Release edit

To reduce the possibility of endless recursion, GitHub Actions ignores events which are triggered using the
auto-generated `GITHUB_TOKEN` access token. All release creations are done automatically by the "Arduino IDE" GitHub
Actions workflow, which uses this token.

For this reason, the release creation trigger will never be used. Since the behavior of the event being ignored by
GitHub Actions under these conditions is not at all obvious, having the workflow configured for such an irrelevant
trigger can cause confusion.

The workflow will be triggered by the manual edit which is done on every release to format the raw release notes
auto-generated from the commit history. So the fact that the release creation trigger doesn't work is not a problem.
2022-03-04 00:41:24 -08:00
Mark Sujew
e6b9d4e2aa Override the RELOAD_REQUESTED_SIGNAL correctly (#880) 2022-03-03 14:37:37 +00:00
Francesco Stasi
93a374d0c6 add PR template file (#838)
* add PR template file

* Update .github/PULL_REQUEST_TEMPLATE.md

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

* Update .github/PULL_REQUEST_TEMPLATE.md

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

Co-authored-by: per1234 <accounts@perglass.com>
2022-03-03 13:54:05 +00:00
Alberto Iannaccone
0fc7c78e11 Install Node.js 14.x on compose-full-changelog workflow (#878)
* Install Node.js 14.x on compose-full-changelog workflow

* change date formate in changelog file name

* improve node js installation in workflow
2022-03-03 08:38:48 +00:00
Alberto Iannaccone
96b5edf427 fix IDE updater commands (#872)
* fix IDE updater commands

* reinitialise autoupdate when preferences change

* fix typo + add i18n strings
2022-03-01 16:34:43 +00:00
Alberto Iannaccone
a5a6a0b611 Go to download page when automatic update fails (#871)
* add preference to set a custom update url

* go to download page when update fails

* fix i18n check
2022-03-01 08:24:29 +00:00
Alberto Iannaccone
2a27a14a68 put Linux build files inside a folder before zipping (#870)
* add preference to set a custom update url

* put linux build inside a folder before zipping
2022-03-01 08:23:56 +00:00
Alberto Iannaccone
f2d492b5dc show represented file on MacOS (#868) 2022-03-01 08:17:05 +00:00
Alberto Iannaccone
5979e5aad2 add preference to set a custom update url (#865) 2022-02-28 14:04:54 +00:00
Alberto Iannaccone
baa9b5f7ab Automatically check for updates only once (#863)
* Automatically check for updates only once

* set windows version to 2019 on CI
2022-02-24 14:04:36 +00:00
Francesco Stasi
481497e384 Disable autodownload of updates on startup (#860) 2022-02-24 10:43:10 +00:00
Mark Sujew
0207778373 Enable opening the IDE from finder/explorer (#835)
* Enable opening the IDE from finder/explorer

* Make opening windows from args a bit more lenient
2022-02-23 16:39:27 +01:00
Francesco Stasi
d79f32efd7 bump vscode-arduino-tools (#859) 2022-02-23 16:07:40 +01:00
Francesco Stasi
3ab03dd62f Avoid duplicated yaml entries (#858) 2022-02-23 15:55:04 +01:00
Mark Sujew
bc3cb0c230 Save preferences in sequence (#856) 2022-02-23 11:08:19 +01:00
Alberto Iannaccone
473cb11053 Remove target section from electron-builder config (#853)
* remove target section from electron-builder config

* do not modify zip structure before moving to artifcats folder
2022-02-22 11:14:11 +00:00
Alberto Iannaccone
0a87fd00f3 IDE updater bugfixes (#846)
* IDE updater assorted bugfix

- add linux AppImage target
- fix hardcoded if condition that causes to always show the update dialog
- fix redundant test build version
- recalculate sha512 after notarization on macOS

* boost notarization speed

* recalculate artifacts hash
2022-02-21 21:40:46 +00:00
Alberto Iannaccone
9b1f15def8 upgrade IDE to rc4 (#841) 2022-02-17 10:39:39 +00:00
Alberto Iannaccone
77b430675d fix generation of updater channel files in CI (#840) 2022-02-17 09:29:56 +00:00
Alberto Iannaccone
f660058c75 Check for IDE update at startup (#797)
* Remove check for updates on startup setting

* Remove useless exported function

* Update template-package.json used to package IDE

* Add function to get channel file during packaging step

* Add updates check

* move ide updater on backend

* configure updater options

* add auto update preferences

* TMP check updates on start and download

* index on check-update-startup: fcb8f6e TMP check updates on start and download

* set version to skip on local storage

* add IDE setting to toggle update check on start-up

* comment out check for updates on startup and auto update settings

* Update Theia to 1.22.1

* updated CI

* download changelog and show it in IDE updater dialog

* remove useless file

* remove useless code

* add i18n to updater dialog

* fix i18n

* refactor UpdateInfo typing

* add macos zip to artifacts

* Simply use `--ignore-engines`

* Use correct --ignore-engines

* Fix semver#valid call

* Use C++17

* updated documentation

* add update channel preference

* update updater url

* updated documentation

* Fix the C++ version

* Build flag for cpp

* add disclaimer with correct node version

* Update `electron-builder`

* Fix `Electron.Menu` issue

* Skip electron rebuild

* Rebuild native dependencies beforehand

* Use resolutions section

* Update template-package.json as well

* move ide-updater to electron application

* refactor ide-updater service

* update yarn.lock

* update i18n

* Revert "Add gRPC user agent (#834)"

This reverts commit 5ab3a747a6.

* fix ide download url

* update latest file in CI

* fix i18n check

Co-authored-by: Silvano Cerza <silvanocerza@gmail.com>
Co-authored-by: Francesco Stasi <f.stasi@me.com>
Co-authored-by: Mark Sujew <msujew@yahoo.de>
2022-02-15 17:01:19 +00:00
Silvano Cerza
9ecff86bbe Fix version retrieval in node process (#837) 2022-02-15 16:52:13 +01:00
Silvano Cerza
5ab3a747a6 Add gRPC user agent (#834) 2022-02-14 12:39:48 +01:00
Silvano Cerza
877c1a1559 Fix board options not shown for manually installed platforms (#826) 2022-02-14 10:12:18 +01:00
Alberto Iannaccone
2f9bf86d75 update arduino-cli to 0.21.0 (#820) 2022-02-11 14:50:56 +00:00
Mark Sujew
112153fb96 Update Theia to 1.22.1 (#791) 2022-02-11 15:25:35 +01:00
Mark Sujew
69ac1f4779 Open all closed workspaces on startup (#780) 2022-02-11 10:57:44 +01:00
Ben
a20899ff43 When a new port is connected and checking to connect to it because previously connected board matches the name / fqbn, also check that the protocol matches. (#792) 2022-02-01 14:35:21 +01:00
Silvano Cerza
ef2be1c086 Small code fix 2022-01-31 17:29:56 +01:00
Silvano Cerza
af33dce0f6 Solve ports conflicts with same address and different protocol 2022-01-31 17:29:56 +01:00
Silvano Cerza
b3b22795f8 Fix compose-changelog.js overwriting itself when called with no arguments 2022-01-27 18:42:34 +01:00
Silvano Cerza
8a0454db51 Fix compose full changelog workflow 2022-01-27 18:10:30 +01:00
Silvano Cerza
f1a5d87ab2 Full changelog is now created from separate workflow 2022-01-27 16:56:03 +01:00
Silvano Cerza
cf0a2161af Add step to generate full changelog on release 2022-01-27 16:56:03 +01:00
Silvano Cerza
dcebd863cc Changelog file is now written to file 2022-01-27 16:56:03 +01:00
Silvano Cerza
e8477b14f3 Fix substitutions issues with compose-changelog script 2022-01-27 16:56:03 +01:00
Alberto Iannaccone
0230071b5f add script to compose full changelog 2022-01-27 16:56:03 +01:00
Alberto Iannaccone
1d88263c85 update ls to 0.6.0and clangd to 13.0.0 (#738) 2022-01-24 16:21:19 +00:00
Francesco Stasi
a71ac4c44d Update BUILDING.md 2022-01-21 10:47:12 +01:00
per1234
66fc27e58c Remove stray brace from compilation error output
An extra brace was inadvertently introduced into a template literal used to format output text in the event of an error
during compilation. This caused the text to end in a pointless `}`

For example:

```
Compilation error: exit status 1}
```

After this change, the output text is as expected:

```
Compilation error: exit status 1
```
2022-01-17 02:46:40 -08:00
per1234
bc365f4a8d Correct minor typos in UI text and documentation 2022-01-17 02:16:36 -08:00
per1234
a5891f9884 Update development docs for current repository
The original location of the project repository was `bcmi-labs/arduino-editor` and some of the internal development
documentation for the project contains references to the repository.

This documentation was not updated at the time the repository was moved to the current home in `arduino/arduino-ide`.
2022-01-17 02:16:08 -08:00
Francesco Stasi
fcdf16a937 Update BUILDING.md 2022-01-14 12:12:17 +01:00
github-actions[bot]
e0b6dbbf2a Updated translation files (#723)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-01-13 17:03:42 +01:00
Francesco Stasi
9529e78647 Improve build instructions (#706) 2022-01-13 17:02:45 +01:00
Francesco Stasi
51da3c0668 Version 2.0.0-rc3 2021-12-22 16:44:17 +01:00
Francesco Stasi
c00d3d33dd Merge remote-tracking branch 'origin/i18n/translations-update' 2021-12-22 16:43:22 +01:00
Francesco Stasi
cfa9b8aea6 bump serial plotter to 0.0.17 2021-12-22 11:32:44 +01:00
per1234
6106e9ff1a Use major version ref of carlosperate/download-file-action
The `carlosperate/download-file-action` action is used in the GitHub Actions workflows as a convenient way to download
external resources.

A major version ref has been added to that repository. It will always point to the latest release of the "1" major
version series. This means it is no longer necessary to do a full pin of the action version in use as before.

Use of the major version ref will cause the workflow to use a stable version of the action, while also benefiting from
ongoing development to the action up until such time as a new major release of an action is made. At that time we would
need to evaluate whether any changes to the workflow are required by the breaking change that triggered the major
release before manually updating the major ref (e.g., uses: `carlosperate/download-file-action@v2`). I think this
approach strikes the right balance between stability and maintainability for these workflows.
2021-12-21 01:19:29 -08:00
Francesco Stasi
b1d9f65a0d bump serial plotter version (#698) 2021-12-20 15:49:16 +01:00
Francesco Stasi
f4008100e1 Correctly transform uint8array to string (#696)
* correctly transform uint8array to string

* export function
2021-12-20 14:56:38 +01:00
Francesco Stasi
11a6959a24 serial monitor lines not to wrap (#697) 2021-12-20 14:56:26 +01:00
github-actions[bot]
3c6e11832b Updated translation files 2021-12-20 02:19:55 +00:00
Alberto Iannaccone
c064673ce1 Close serial port connection before flashing firmware (#688) 2021-12-15 09:31:12 +00:00
Silvano Cerza
cc5764e536 Update README.md
Co-authored-by: per1234 <accounts@perglass.com>
2021-12-14 17:47:31 +01:00
Silvano Cerza
9131f2d09e Update README.md with translations project link 2021-12-14 17:47:31 +01:00
Alberto Iannaccone
0b6fc0b973 Version 2.0.0-rc2 2021-12-13 11:04:35 +01:00
Alberto Iannaccone
c91fe2d775 bump arduino-language-server to 0.5.0 (#679) 2021-12-13 09:55:50 +00:00
github-actions[bot]
bbded57ae4 Updated translation files (#638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-12-13 09:20:03 +01:00
Francesco Stasi
a8ae0bb4e0 workaround: stop discoveries before install/uninstall boards/libs (#674) 2021-12-10 17:03:24 +01:00
Francesco Stasi
49d12d99ff IDE to run CLI with auto assigned port (#673)
* get daemon port from CLI stdout

* config-service to use CLI daemon port

* updating LS

* fixed tests

* fix upload blocked when selectedBoard.port is undefined

* bump arduino-cli to 0.20.2

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-12-09 15:08:26 +01:00
Francesco Stasi
767b09d2f1 Fix upload and serial (#661)
* get serial connection status from BE

* handle serial connect in the BE

* allow breakpoints on vscode (windows)

* Timeout on config change to prevent serial busy

* serial-service tests
2021-12-07 17:38:43 +01:00
Alberto Iannaccone
88397931c5 Automatically install 'Arduino_BuiltIn' library at first startup (#663) 2021-12-06 15:56:17 +00:00
Silvano Cerza
5ddab1ded7 Remove gRPC error code from error notifications 2021-12-06 09:58:17 +01:00
Francesco Stasi
f0d9894a16 Fix notification icons (#642) 2021-11-30 17:24:29 +01:00
Alberto Iannaccone
59e4c57ecd Update version to 2.0.0-rc1 2021-11-30 12:21:59 +01:00
Francesco Stasi
dd76f9180c Update Theia, CLI and LS (#610)
* Update Theia to 1.19.0

* update CLI to 0.20.0-rc3

* Add language selector to settings

* updated language server and vscode-arduino-tools

* update Language Server flags

* get cli port from config

* force native menu on windows

* pinned Language Server to rc2

* fix search icon

* update CLI version
2021-11-29 15:54:13 +01:00
Alberto Iannaccone
6e34a27b7e move language server preference to advanced 2021-11-29 15:03:03 +01:00
Silvano Cerza
a090dfe99c Add dialog to insert user fields for board that require them to upload (#550)
* Rebuild gRPC protocol interfaces

* Implement methods to get user fields for board/port combination

* Implement dialog to input board user fields

* Add configure and upload step when uploading to board requiring user fields

* Disable Sketch > Configure and Upload menu if board doesn't support user fields

* Fix serial upload not working with all boards

* Update i18n source file

* fix user fields UI

* regenerate cli protocol

* fix localisation

* check if user fields are empty

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-11-25 18:22:51 +01:00
Silvano Cerza
74bfdc4c56 Rework listing of discovered ports (#614)
* Removed Protocol type

* Reworked function that groups ports by protocol

* Remove useless protocol check in Port sameAs function

* Reworked port selection menu ordering

Now ports are shown in this order:
1. Serial with recognized boards
2. Serial with unrecognized boards
3. Network with recognized boards
4. Network with unrecognized boards
5. Other protocols with recognized boards
6. Other protocols with unrecognized boards

* Fix ports shown multiple times in menu

* Reworked board selection dropdown ordering

Ordering is now:
1. Serial with recognized boards
2. Serial with guessed boards
3. Serial with incomplete boards
4. Network with recognized boards
5. Other protocols with recognized boards

* Localize some strings

* Fix bug selecting board in boards selector dropdown

* Reworked board selection dialog ordering

* Fix Tools > Port menu not refreshing

* Move Select other board button to bottom of Board selector dropdown and change its style

* Updated arduino-cli to 0.20.0 and generated protocol files
2021-11-24 15:15:40 +01:00
Alberto Iannaccone
20f7712129 Serial Plotter implementation (#597)
* spawn new window where to instantiate serial plotter app

* initialize serial monito web app

* connect serial plotter app with websocket

* use npm serial-plotter package

* refactor monitor connection and fix some connection issues

* fix clearConsole + refactor monitor connection

* add serial unit tests

* refactoring and cleaning code
2021-11-23 17:18:20 +00:00
Francesco Stasi
9863dc2f90 Fix editor tabs order (#612) 2021-11-23 12:16:56 +01:00
Francesco Stasi
13734a642c Disable Editor breadcrumbs by default (#611) 2021-11-23 12:14:45 +01:00
Silvano Cerza
7ac7ae9063 Fix i18n:generate command not including tsx files 2021-11-17 18:17:03 +01:00
Federico Bond
437caeb348 Open Save as... dialog when saving sketches for the first time (#579)
* Properly recognize temporary sketches in macOS

Without this fix, sketches report their URI path as /private/var/xxx
whereas `os.tmpdir()` returns /var/xxx. The second path can be turned
into the first by resolving symlinks, which gives a canonical path to
compare against.

* Open Save as... dialog when saving sketches for the first time
2021-11-10 15:46:24 +00:00
Silvano Cerza
3b04d8df26 Remove gRPC errors codes from compile/upload console output (#564) 2021-11-05 10:08:06 +01:00
Silvano Cerza
99d65531c4 Update translation source file 2021-11-05 09:49:05 +01:00
Silvano Cerza
4f4ccb8c66 Add step to install dependencies in i18n workflows 2021-11-05 09:49:05 +01:00
Silvano Cerza
7bc83eba1d Update theia/cli version 2021-11-05 09:49:05 +01:00
Silvano Cerza
72750f0be3 Update .github/workflows/check-i18n-task.yml
Co-authored-by: per1234 <accounts@perglass.com>
2021-11-05 09:49:05 +01:00
Silvano Cerza
8cbf7f419c Apply suggestions from code review
Co-authored-by: per1234 <accounts@perglass.com>
2021-11-05 09:49:05 +01:00
Silvano Cerza
ea2aeec69b Add workflows to push and pull translations from Transifex and check source file is updated when necessary 2021-11-05 09:49:05 +01:00
Silvano Cerza
b83702fde3 Add commands to generate translation file and check they're updated 2021-11-05 09:49:05 +01:00
Silvano Cerza
5be3e9de2d Add script to push translations source to transifex 2021-11-05 09:49:05 +01:00
Silvano Cerza
e8bc7d7179 Add script to download translations from transifex 2021-11-05 09:49:05 +01:00
Mark Sujew
acbb164c3c Fix cortex-debug related debugging issue (#578)
* Fix cortex-debug related debugging issue

* Update arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts

Co-authored-by: Francesco Stasi <francescomaria.stasi@gmail.com>
2021-10-27 12:56:17 +02:00
Silvano Cerza
99099b06aa Fix duplicated id children warnings 2021-10-20 11:28:23 +02:00
Silvano Cerza
5c958bc6c7 Fix Tools > Board and Tools > Port labels (#558) 2021-10-18 11:35:26 +02:00
Mark Sujew
11b75bd610 Translating Arduino-IDE using Theia's nls API (#545) 2021-10-18 09:59:33 +02:00
Francesco Stasi
61262c23ac fix: reset charCount on serial monitor reset 2021-10-15 00:11:26 +02:00
per1234
7503739a9f Sync labels in write mode on schedule trigger
In order to facilitate the testing and review of proposed changes to the repository label infrastructure, the
"Sync Labels" template workflow does a dry run when triggered under conditions that indicate it would not be appropriate
to make real changes to the repository's labels. The changes that would have resulted are printed to the log, but not
actually made.

One of the criteria used to determine "dry run" mode usage is whether the event occurred on the repository's default
branch. A trigger on a development branch or for a pull request should not result in a change to the labels.
It turns out that GitHub does not define a `github.event.repository.default_branch` context item when a workflow is
triggered by a `schedule` event. This resulted in the workflow always running in "dry run" mode on a `schedule` trigger.
Since `schedule` and `repository_dispatch` triggers are only permitted for the default branch, there is no need to check
whether the event's ref matches the default branch and it is safe to always run in write mode on these events.
2021-10-13 01:57:33 -07:00
per1234
060ab5bccb Correct context key name in "Sync Labels" workflow
Incorrect context key name resulted in impossible to satisfy conditional, meaning the dry run determination code was
solely dependent on the check for whether the workflow was triggered from the default branch name.
2021-10-13 01:57:33 -07:00
Steve Anderson
1c42b8cefc Footer min-height for library and board manager (#392)
Increase the `min-height` from 26px to 30px to prevent the list items from changing height when mousing over them
2021-10-07 16:39:26 +01:00
Francesco Stasi
825f0b0f2a Updated to 2.0.0-beta.12 2021-10-07 09:38:19 +02:00
Francesco Stasi
846c22cb03 Theia 18 hotfixes (#528)
* Restore monaco suggestion highlights

* remove duplicated tabs on startup

* fix rename and delete sketch

* remove '.only(...)' in tests

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-10-06 16:50:02 +01:00
Francesco Stasi
fc0f67493b [ATL-1599] [ATL-1416] Upgrade Theia to 1.18.0 (#489)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-10-06 13:55:55 +02:00
Francesco Stasi
54a67fc67c Improve Serial Monitor Performances (#524)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-10-06 09:21:06 +02:00
Alberto Iannaccone
7f8b227c39 [ATL-1531] Integrate arduino-cli 0.19.1 (#506)
* integrate cli 0.19.0

* Update CLI version used to fix crash on lib/core install/uninstall

* Update CLI version

* Update CLI version

* update cli version

Co-authored-by: Silvano Cerza <silvanocerza@gmail.com>
2021-09-30 09:02:09 +01:00
Silvano Cerza
ba177be41d [skip changelog] Add missing athena script 2021-09-27 18:14:06 +02:00
Silvano Cerza
0eb2d25570 [skip changelog] Update workflow and script to fetch Arduino CDN download data 2021-09-27 18:07:32 +02:00
Alberto Iannaccone
e9db1c0482 implement unit tests for boards-auto-installer (#513)
Co-authored-by: Francesco Stasi <f.stasi@me.com>
2021-09-27 10:09:11 +01:00
per1234
79b075c961 Add CI workflow to synchronize with shared repository labels
On every push that changes relevant files, and periodically, configure the repository's issue and pull request labels
according to the universal, shared, and local label configuration files.
2021-09-24 10:01:57 -07:00
rsora
a46f36acd1 [skip changelog] Add stats workflow to gather downloads data 2021-09-24 18:11:06 +02:00
Alberto Iannaccone
bfb90a8b4f at first ide startup invoke installation of arduino:avr (#497) 2021-09-02 11:50:26 +01:00
Alberto Iannaccone
658c19f55b [ATL-1571] Fix editor quick suggestions preference (#494)
* Fix editor quick suggestions preference

* little settings refactoring
2021-09-02 11:50:04 +01:00
Alberto Iannaccone
3f8a07654d add refresh icon to fontawesome (#493) 2021-09-02 11:49:44 +01:00
Alberto Iannaccone
a8ec7c2640 Change menu item "Export compiled Binary" to "Export Compiled Binary" (#492) 2021-09-02 11:49:16 +01:00
Alberto Iannaccone
a7a1f95ced Adjust "Edit" menu to remove "Copy for Forum"/"Copy for GitHub" redundancy (#491) 2021-09-02 11:48:42 +01:00
Yash
835e9913ae Fix README broken link (#467)
I believe this file name " path src/node/monitor-service-impl.ts " was moved into another folder named monitor, making the correct path for this file here "src/node/monitor/monitor-service-impl.ts"
2021-08-31 15:55:47 +02:00
Francesco Stasi
d3d6ba8176 [ATL-1556] Sort board families in Tool menu (#486)
* [ATL-1556] Sort board families in Tool menu
2021-08-26 15:25:37 +02:00
Francesco Stasi
0f82e91380 [ATL-1570] Install core notification not to appear on board unplug (#485) 2021-08-26 15:09:56 +02:00
Francesco Stasi
7d5381bbde Updated to 2.0.0-beta.11 2021-08-25 10:43:10 +02:00
Francesco Stasi
302fb7b6af [ATL-1533] Firmware&Certificate Uploader (#469)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-08-25 10:36:51 +02:00
Alberto Iannaccone
6233e1fa98 [ATL-493] Support platforms installed in directories.user 2021-08-23 10:47:36 +02:00
per1234
2cb9889fe4 Add source URL comment to "Check Certificates" workflow
This will make it easier for the maintainers to sync fixes and improvements in either direction between the upstream
"template" workflow and its installation in this repository.
2021-08-18 03:00:24 -07:00
per1234
bed6e0b741 Use major version ref of Slack notification action
At the time the workflow was written the authors of the `rtCamp/action-slack-notify` GitHub Actions action did not
provide a major version ref. This meant that it was necessary to pin the action to a specific version.

Since then, a few new releases have been made, meaning an outdated version of the action was in use as a consequence of
the pinning.

The action now offers a `v2` major ref. Use of this ref will cause the workflow to benefit from ongoing development to
the action up until such time as a new major release is made, at which time we would need to evaluate whether any changes
to the workflow are required by the breaking change that triggered the major release before updating the major ref
(e.g., `uses: rtCamp/action-slack-notify@v3`).
2021-08-18 03:00:24 -07:00
per1234
302f0109dd Use standardized repository secret name for Slack webhook
The "Check Certificates" workflow is configured to send a notification via Slack if a problem is found with a certificate.
TThis is currently posted to the `team_tooling` channel, but that is not necessarily always going to be the case, and for
every deployment of the workflow. So a less specific secret name is more universally applicable to serve all applications
of this "template" workflow.
2021-08-18 03:00:24 -07:00
per1234
735d3733e2 Make trivial formatting changes to "Check Certificates" workflow
No functional change, and neither is necessarily superior, but this is the formatting style either defined in the
"template", or by the repository's Prettier formatting configuration preferences, so it must be brought into compliance
here as well.
2021-08-18 03:00:24 -07:00
per1234
4b36852f57 Use the matrix identifier to name the "Check Certificates" workflow jobs
When no name is provided for a matrix job, the workflow job is named according to the contents of
`jobs[].<job_id>.strategy.matrix[]`. That can result in some fairly cryptic job names when the matrix contains a complex
data structure as is the case here. We already have a string to uniquely identify each certificate to humans, which is
exactly what the `jobs[].<job_id>.name` property does for jobs, so it will be an improvement to name the jobs according
to that identifier.
2021-08-18 03:00:24 -07:00
per1234
b84b6c921d Make trivial adjustments to comments in "Check Certificates" workflow
No functional difference, and neither is necessarily superior, but this is how it is in the "template", and so it must be
here as well.
2021-08-18 03:00:24 -07:00
per1234
289f07f187 Run "Check Certificates" workflow on modification
This will facilitate testing and review of modifications to the workflow.

Because the workflow requires access to repository secrets, and so will fail whenever triggered by an event from a fork,
a conditional is added to make it only run when the modifications are made within the `arduino/arduino-ide`
repository.
2021-08-18 03:00:24 -07:00
per1234
b9c777a5c3 Add API trigger to "Check Certificates" workflow
The `repository_dispatch` event allows triggering workflows via the GitHub API. This might be useful for triggering an
immediate check in multiple relevant repositories after an external change, or some automated process. Although we don't
have any specific need for this event at the moment, the event has no impact on the workflow, so there is no reason
against having it. It is the sort of thing that can end up being useful if it is already in consistently in place, but
not worth setting up on demand, since the effort to set it up is greater than the effort to trigger all the workflows
manually.
2021-08-18 03:00:24 -07:00
per1234
92af4bef26 Use standardized name for certificate check workflow
This is the naming convention established in the standardized "template" workflow.
2021-08-18 03:00:24 -07:00
Jim Marinis
167f059163 Update BUILDING.md
Corrected typographical error where "on" was used rather than "one".
2021-08-06 05:16:05 -07:00
402 changed files with 43282 additions and 14736 deletions

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

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

View File

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

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

@@ -0,0 +1,13 @@
# Source:
# https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/template-choosers/general/config.yml
blank_issues_enabled: false
contact_links:
- name: Learn about using this project
url: https://github.com/arduino/arduino-ide#readme
about: Detailed usage documentation is available here.
- name: Support request
url: https://forum.arduino.cc/
about: We can help you out on the Arduino Forum!
- name: Discuss development work on the project
url: https://groups.google.com/a/arduino.cc/g/developers
about: Arduino Developers Mailing List

View File

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

View File

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

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,15 @@
### Motivation
<!-- Why this pull request? -->
### Change description
<!-- What does your code do? -->
### Other information
<!-- Any additional information that could help the review process -->
### Reviewer checklist
* [ ] PR addresses a single concern.
* [ ] The PR has no duplicates (please search among the [Pull Requests](https://github.com/arduino/arduino-ide/pulls) before creating one)
* [ ] PR title and description are properly filled.
* [ ] Docs have been added / updated (for bug fixes / features)

View File

@@ -0,0 +1,24 @@
# Used by the "Sync Labels" workflow
# See: https://github.com/Financial-Times/github-label-sync#label-config-file
- name: "topic: accessibility"
color: "00ffff"
description: Enabling the use of the software by everyone
- name: "topic: CLI"
color: "00ffff"
description: Related to Arduino CLI
- name: "topic: debugger"
color: "00ffff"
description: Related to the integrated debugger
- name: "topic: language server"
color: "00ffff"
description: Related to the Arduino Language Server
- name: "topic: serial monitor"
color: "00ffff"
description: Related to the Serial Monitor
- name: "topic: theia"
color: "00ffff"
description: Related to the Theia IDE framework
- name: "topic: theme"
color: "00ffff"
description: Related to GUI theming

131
.github/tools/fetch_athena_stats.py vendored Normal file
View File

@@ -0,0 +1,131 @@
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}")

57
.github/workflows/arduino-stats.yaml vendored Normal file
View File

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

@@ -15,15 +15,15 @@ on:
env:
JOB_TRANSFER_ARTIFACT: build-artifacts
CHANGELOG_ARTIFACTS: changelog
jobs:
build:
if: github.repository == 'arduino/arduino-ide'
strategy:
matrix:
config:
- os: windows-latest
- os: windows-2019
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
- os: macos-latest
runs-on: ${{ matrix.config.os }}
@@ -33,16 +33,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js 12.x
- name: Install Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: '12.14.1'
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Python 2.7
- name: Install Python 3.x
uses: actions/setup-python@v2
with:
python-version: '2.7'
python-version: '3.x'
- name: Package
shell: bash
@@ -50,34 +50,37 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
run: |
# See: https://www.electron.build/code-signing
if [ $IS_FORK = true ]; then
echo "Skipping the app signing: building from a fork."
else
if [ "${{ runner.OS }}" = "macOS" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
# See: https://www.electron.build/code-signing
if [ $IS_FORK = true ]; then
echo "Skipping the app signing: building from a fork."
else
if [ "${{ runner.OS }}" = "macOS" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
elif [ "${{ runner.OS }}" = "Windows" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
elif [ "${{ runner.OS }}" = "Windows" ]; then
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
npm config set msvs_version 2017 --global
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
fi
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
fi
fi
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
npx node-gyp install
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v2
@@ -94,15 +97,19 @@ jobs:
strategy:
matrix:
artifact:
- path: "*Linux_64bit.zip"
name: Linux_X86-64
- path: "*macOS_64bit.dmg"
name: macOS
- path: "*Windows_64bit.exe"
- path: '*Linux_64bit.zip'
name: Linux_X86-64_zip
- path: '*Linux_64bit.AppImage'
name: Linux_X86-64_app_image
- path: '*macOS_64bit.dmg'
name: macOS_dmg
- path: '*macOS_64bit.zip'
name: macOS_zip
- path: '*Windows_64bit.exe'
name: Windows_X86-64_interactive_installer
- path: "*Windows_64bit.msi"
- path: '*Windows_64bit.msi'
name: Windows_X86-64_MSI
- path: "*Windows_64bit.zip"
- path: '*Windows_64bit.zip'
name: Windows_X86-64_zip
steps:
@@ -111,7 +118,7 @@ jobs:
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
- name: Upload tester build artifact
uses: actions/upload-artifact@v2
with:
@@ -134,24 +141,24 @@ jobs:
env:
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
export LATEST_TAG=$(git describe --abbrev=0)
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
if [ "$IS_RELEASE" = true ]; then
export BODY=$(echo -e "$GIT_LOG")
else
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
if [ -z "$GIT_LOG" ]; then
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
else
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
fi
export LATEST_TAG=$(git describe --abbrev=0)
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
if [ "$IS_RELEASE" = true ]; then
export BODY=$(echo -e "$GIT_LOG")
else
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
if [ -z "$GIT_LOG" ]; then
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
else
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
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"
echo "$BODY" > CHANGELOG.txt
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"
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
@@ -174,9 +181,9 @@ jobs:
- name: Publish Nightly [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
PLUGIN_TARGET: "/arduino-ide/nightly"
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide/nightly'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -210,9 +217,9 @@ jobs:
- name: Publish Release [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
PLUGIN_TARGET: "/arduino-ide"
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -1,35 +1,41 @@
name: Check for issues with signing certificates
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-certificates.md
name: Check Certificates
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
push:
paths:
- '.github/workflows/check-certificates.ya?ml'
pull_request:
paths:
- '.github/workflows/check-certificates.ya?ml'
schedule:
# run every 10 hours
- cron: "0 */10 * * *"
# workflow_dispatch event allows the workflow to be triggered manually.
# This could be used to run an immediate check after updating certificate secrets.
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
# Run every 10 hours.
- cron: '0 */10 * * *'
workflow_dispatch:
repository_dispatch:
env:
# Begin notifications when there are less than this many days remaining before expiration
# Begin notifications when there are less than this many days remaining before expiration.
EXPIRATION_WARNING_PERIOD: 30
jobs:
check-certificates:
name: ${{ matrix.certificate.identifier }}
# Only run when the workflow will have access to the certificate secrets.
if: >
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
certificate:
- identifier: macOS signing certificate # Text used to identify the certificate in notifications
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password
# Additional certificate definitions can be added to this list.
- identifier: macOS signing certificate # Text used to identify certificate in notifications.
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
- identifier: Windows signing certificate
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
@@ -37,7 +43,7 @@ jobs:
steps:
- name: Set certificate path environment variable
run: |
# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
# See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
- name: Decode certificate
@@ -59,18 +65,17 @@ jobs:
exit 1
)
# See: https://github.com/rtCamp/action-slack-notify
- name: Slack notification of certificate verification failure
if: failure()
uses: rtCamp/action-slack-notify@v2.1.0
env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_MESSAGE: |
:warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
:warning::warning::warning::warning:
SLACK_COLOR: danger
MSG_MINIMAL: true
uses: rtCamp/action-slack-notify@v2
- name: Get days remaining before certificate expiration date
env:
@@ -99,7 +104,7 @@ jobs:
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
# Display the expiration information in the log
# Display the expiration information in the log.
echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
@@ -114,14 +119,14 @@ jobs:
fi
- name: Slack notification of pending certificate expiration
# Don't send spurious expiration notification if verification fails
# Don't send spurious expiration notification if verification fails.
if: failure() && steps.check-expiration.outcome == 'failure'
uses: rtCamp/action-slack-notify@v2.1.0
env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_MESSAGE: |
:warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
:warning::warning::warning::warning:
SLACK_COLOR: danger
MSG_MINIMAL: true
uses: rtCamp/action-slack-notify@v2

38
.github/workflows/check-i18n-task.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Check Internationalization
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
push:
paths:
- '.github/workflows/check-i18n-task.ya?ml'
- '**/package.json'
- '**.ts'
- 'i18n/**'
pull_request:
paths:
- '.github/workflows/check-i18n-task.ya?ml'
- '**/package.json'
- '**.ts'
- 'i18n/**'
workflow_dispatch:
repository_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn
- name: Check for errors
run: yarn i18n:check

View File

@@ -0,0 +1,55 @@
name: Compose full changelog
on:
release:
types:
- edited
env:
CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 14.x
jobs:
create-changelog:
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
- name: Create full changelog
id: full-changelog
run: |
yarn add @octokit/rest --ignore-workspace-root-check
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
# Get the changelog file name to build
CHANGELOG_FILE_NAME="${{ steps.tag_name.outputs.TAG_NAME }}-$(date +%s).md"
# Create manifest file pointing to latest changelog file name
echo "$CHANGELOG_FILE_NAME" >> "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/latest.txt"
# Compose changelog
yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME"
- name: Publish Changelog [S3]
uses: docker://plugins/s3
env:
PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/'
PLUGIN_TARGET: '/arduino-ide/changelog'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

96
.github/workflows/github-stats.yaml vendored Normal file
View File

@@ -0,0 +1,96 @@
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 }}"

30
.github/workflows/i18n-nightly-push.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: i18n-nightly-push
on:
schedule:
# run every day at 1AM
- cron: '0 1 * * *'
jobs:
push-to-transifex:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn
- name: Run i18n:push script
run: yarn run i18n:push
env:
TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}

38
.github/workflows/i18n-weekly-pull.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: i18n-weekly-pull
on:
schedule:
# run every monday at 2AM
- cron: '0 2 * * 1'
jobs:
pull-from-transifex:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn
- name: Run i18n:pull script
run: yarn run i18n:pull
env:
TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
commit-message: Updated translation files
title: Update translation files
branch: i18n/translations-update
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

139
.github/workflows/sync-labels.yml vendored Normal file
View File

@@ -0,0 +1,139 @@
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/sync-labels.md
name: Sync Labels
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
push:
paths:
- ".github/workflows/sync-labels.ya?ml"
- ".github/label-configuration-files/*.ya?ml"
pull_request:
paths:
- ".github/workflows/sync-labels.ya?ml"
- ".github/label-configuration-files/*.ya?ml"
schedule:
# Run daily at 8 AM UTC to sync with changes to shared label configurations.
- cron: "0 8 * * *"
workflow_dispatch:
repository_dispatch:
env:
CONFIGURATIONS_FOLDER: .github/label-configuration-files
CONFIGURATIONS_ARTIFACT: label-configuration-files
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download JSON schema for labels configuration file
id: download-schema
uses: carlosperate/download-file-action@v1
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
- name: Install JSON schema validator
run: |
sudo npm install \
--global \
ajv-cli \
ajv-formats
- name: Validate local labels configuration
run: |
# See: https://github.com/ajv-validator/ajv-cli#readme
ajv validate \
--all-errors \
-c ajv-formats \
-s "${{ steps.download-schema.outputs.file-path }}" \
-d "${{ env.CONFIGURATIONS_FOLDER }}/*.{yml,yaml}"
download:
needs: check
runs-on: ubuntu-latest
strategy:
matrix:
filename:
# Filenames of the shared configurations to apply to the repository in addition to the local configuration.
# https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/sync-labels
- universal.yml
- tooling.yml
steps:
- name: Download
uses: carlosperate/download-file-action@v1
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
with:
path: |
*.yaml
*.yml
if-no-files-found: error
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
sync:
needs: download
runs-on: ubuntu-latest
steps:
- name: Set environment variables
run: |
# See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
echo "MERGED_CONFIGURATION_PATH=${{ runner.temp }}/labels.yml" >> "$GITHUB_ENV"
- name: Determine whether to dry run
id: dry-run
if: >
github.event_name == 'pull_request' ||
(
(
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch'
) &&
github.ref != format('refs/heads/{0}', github.event.repository.default_branch)
)
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"
- name: Checkout repository
uses: actions/checkout@v2
- name: Download configuration files artifact
uses: actions/download-artifact@v2
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v1
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
- name: Merge label configuration files
run: |
# Merge all configuration files
shopt -s extglob
cat "${{ env.CONFIGURATIONS_FOLDER }}"/*.@(yml|yaml) > "${{ env.MERGED_CONFIGURATION_PATH }}"
- name: Install github-label-sync
run: sudo npm install --global github-label-sync
- name: Sync labels
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# See: https://github.com/Financial-Times/github-label-sync
github-label-sync \
--labels "${{ env.MERGED_CONFIGURATION_PATH }}" \
${{ steps.dry-run.outputs.flag }} \
${{ github.repository }}

View File

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

9
.gitignore vendored
View File

@@ -7,7 +7,8 @@ build/
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.
.browser_modules
@@ -16,3 +17,9 @@ yarn*.log
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces

View File

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

70
.vscode/launch.json vendored
View File

@@ -4,17 +4,12 @@
{
"type": "node",
"request": "launch",
"name": "App (Electron)",
"name": "App (Electron) [Dev]",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"cwd": "${workspaceFolder}/electron-app",
"protocol": "inspector",
"args": [
".",
"--log-level=debug",
@@ -23,7 +18,11 @@
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins"
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--nosplash",
"--content-trace",
"--open-devtools"
],
"env": {
"NODE_ENV": "development"
@@ -33,12 +32,55 @@
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "App (Electron)",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "chrome",
"request": "attach",
"name": "Attach to Electron Frontend",
"port": 9222,
"webRoot": "${workspaceFolder}/electron-app"
},
{
"type": "node",
"request": "launch",
@@ -73,12 +115,13 @@
{
"type": "node",
"request": "launch",
"protocol": "inspector",
"name": "Run Test [current]",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [
"--require",
"reflect-metadata/Reflect",
"--require",
"ignore-styles",
"--no-timeouts",
"--colors",
"**/${fileBasenameNoExtension}.js"
@@ -104,5 +147,14 @@
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
}
],
"compounds": [
{
"name": "Launch Electron Backend & Frontend",
"configurations": [
"App (Electron)",
"Attach to Electron Frontend"
]
}
]
}

View File

@@ -14,7 +14,7 @@ The _Electron main_ process is responsible for:
- managing the application lifecycle via listeners, and
- creating and managing the web pages for the app.
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process.
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
@@ -40,22 +40,38 @@ The _frontend_ is running as an Electron renderer process and can invoke service
## Build from source
If youre familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
project, you should be able to build the Arduino IDE locally.
Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
### Build
```sh
yarn
```
Once you have all the tools installed, you can build the editor following these steps
### Rebuild the native dependencies
```sh
yarn rebuild:electron
```
1. Install the dependencies and build
```sh
yarn
```
### Start
```sh
yarn start
```
2. Rebuild the dependencies
```sh
yarn rebuild:browser
```
3. Rebuild the electron dependencies
```sh
yarn rebuild:electron
```
4. Start the application
```sh
yarn start
```
### Notes for Windows contributors
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
### CI
@@ -73,6 +89,7 @@ This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide
git push origin 1.2.3
```
## Notes for macOS contributors
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
@@ -117,7 +134,7 @@ git add . \
git tag -a 0.2.0 -m "0.2.0" \
&& git push origin 0.2.0
```
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
- If you do not like the generated changelog, modify it and update the GH release.

View File

@@ -15,29 +15,31 @@ The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is
## Download
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
### Nightly builds
These builds are generated every day at 03:00 GMT from the `main` branch and
should be considered unstable:
Platform | 32 bit | 64 bit |
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
Linux | | [Nightly Linux 64 bit] |
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
macOS | | [Nightly macOS 64 bit] |
| Platform | 32 bit | 64 bit |
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
| macOS | | [Nightly macOS 64 bit] |
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/107
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, redirecting to latest
generated builds by replacing `latest` with the latest available build
date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
replaced with `20190806`)
> generated builds by replacing `latest` with the latest available build
> date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
> replaced with `20190806`)
## Support
@@ -47,8 +49,8 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
* Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
* Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
### Security
@@ -64,10 +66,13 @@ Contributions are very welcome! You can browse the list of open issues to see wh
This repository contains the main code, but two more repositories are included during the build process:
* [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
* [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
## Donations
This open source code was written by the Arduino team and is maintained on a daily basis with the help of the community. We invest a considerable amount of time in development, testing and optimization. Please consider [donating](https://www.arduino.cc/en/donate/) or [sponsoring](https://github.com/sponsors/arduino) to support our work, as well as [buying original Arduino boards](https://store.arduino.cc/) which is the best way to make sure our effort can continue in the long term.

View File

@@ -30,17 +30,20 @@ The Core Service is responsible for building your sketches and uploading them to
- compiling a sketch for a selected board type
- uploading a sketch to a connected board
#### Monitor Service
#### Serial Service
The Monitor Service allows getting information back from sketches running on your Arduino boards.
The Serial Service allows getting information back from sketches running on your Arduino boards.
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
- [src/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
- [src/common/protocol/serial-service.ts](./src/common/protocol/serial-service.ts) implements the common classes and interfaces
- [src/node/serial/serial-service-impl.ts](./src/node/serial/serial-service-impl.ts) implements the service backend:
- connecting to / disconnecting from a board
- receiving and sending data
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
- [src/browser/serial/serial-connection-manager.ts](./src/browser/serial/serial-connection-manager.ts) handles the serial connection in the frontend
- [src/browser/serial/monitor/monitor-widget.tsx](./src/browser/serial/monitor/monitor-widget.tsx) implements the serial monitor front-end:
- viewing the output from a connected board
- entering data to send to the board
- [src/browser/serial/plotter/plotter-frontend-contribution.ts](./src/browser/serial/plotter/plotter-frontend-contribution.ts) implements the serial plotter front-end:
- opening a new window running the [Serial Plotter Web App](https://github.com/arduino/arduino-serial-plotter-webapp)
#### Config Service
@@ -58,3 +61,13 @@ The Config Service knows about your system, like for example the default sketch
#### Rebuild gRPC protocol interfaces
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol`
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
- load it
- edit the icons as needed
- !! download the **new** `arduino-icons.json` file and put it in this repo
- Click on "Generate Font" in Icomoon, then download
- place the updated fonts in the `src/style/fonts` directory

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,16 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-beta.10",
"version": "2.0.0-rc7",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build",
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-serial-plotter && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
"clean": "rimraf lib",
"compose-changelog": "node ./scripts/compose-changelog.js",
"download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js",
"copy-serial-plotter": "npx ncp ../node_modules/arduino-serial-plotter-webapp ./build/arduino-serial-plotter-webapp",
"copy-i18n": "npx ncp ../i18n ./build/i18n",
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
@@ -18,28 +21,30 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.1.1",
"@theia/application-package": "next",
"@theia/core": "next",
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/keymaps": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/outline-view": "next",
"@theia/preferences": "next",
"@theia/output": "next",
"@theia/search-in-workspace": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/editor-preview": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^5.0.35",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
@@ -49,20 +54,20 @@
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.1.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"deepmerge": "2.0.1",
"fuzzy": "^0.1.3",
"electron-updater": "^4.6.5",
"fast-safe-stringify": "^2.1.1",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
@@ -74,20 +79,29 @@
"open": "^8.0.6",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"query-string": "^7.0.1",
"react-disable": "^0.1.0",
"react-markdown": "^8.0.0",
"react-select": "^3.0.4",
"react-tabs": "^3.1.2",
"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": {
"@octokit/rest": "^18.12.0",
"@types/chai": "^4.2.7",
"@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",
@@ -96,10 +110,13 @@
"download": "^7.1.0",
"grpc_tools_node_protoc_ts": "^4.1.0",
"mocha": "^7.0.0",
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^9.0.1",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
@@ -108,7 +125,8 @@
},
"mocha": {
"require": [
"reflect-metadata/Reflect"
"reflect-metadata/Reflect",
"ignore-styles"
],
"reporter": "spec",
"colors": true,
@@ -137,10 +155,16 @@
],
"arduino": {
"cli": {
"version": "0.18.3"
"version": "0.23.0"
},
"fwuploader": {
"version": "1.0.2"
"version": "2.2.0"
},
"clangd": {
"version": "14.0.0"
},
"languageServer": {
"version": "0.6.0"
}
}
}

View File

@@ -0,0 +1,116 @@
// @ts-check
(async () => {
const { Octokit } = require('@octokit/rest');
const fs = require('fs');
const path = require('path');
const octokit = new Octokit({
userAgent: 'Arduino IDE compose-changelog.js',
});
const response = await octokit.rest.repos
.listReleases({
owner: 'arduino',
repo: 'arduino-ide',
})
.catch((err) => {
console.error(err);
process.exit(1);
});
const releases = response.data;
let fullChangelog = releases.reduce((acc, item, index) => {
// Process each line separately
const body = item.body.split('\n').map(processLine).join('\n');
// item.name is the name of the release changelog
return (
acc +
`## ${item.name}\n\n${body}${
index !== releases.length - 1 ? '\n\n---\n\n' : '\n'
}`
);
}, '');
const args = process.argv.slice(2);
if (args.length == 0) {
console.error('Missing argument to destination file');
process.exit(1);
}
const changelogFile = path.resolve(args[0]);
await fs.writeFile(
changelogFile,
fullChangelog,
{
flag: 'w+',
},
(err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log('Changelog written to', changelogFile);
}
);
})();
// processLine applies different substitutions to line string.
// We're assuming that there are no more than one substitution
// per line to be applied.
const processLine = (line) => {
// Check if a link with one of the following format exists:
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
// If it does return the line as is.
let r =
/(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm;
if (r.test(line)) {
return line;
}
// Check if a issue or PR link with the following format exists:
// * #123
// If it does it's changed to:
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
r = /(?<![\w\d\/_]{1})#((\d)+)(?![\w\d\/_]{1})/gm;
if (r.test(line)) {
return line.replace(
r,
`[#$1](https://github.com/arduino/arduino-ide/pull/$1)`
);
}
// Check if a link with one of the following format exists:
// * https://github.com/arduino/arduino-ide/pull/123
// * https://github.com/arduino/arduino-ide/issues/123
// * https://github.com/arduino/arduino-ide/pull/123/
// * https://github.com/arduino/arduino-ide/issues/123/
// If it does it's changed respectively to:
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
r =
/(https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?)/gm;
if (r.test(line)) {
return line.replace(r, `[#$3]($1)`);
}
// Check if a link with the following format exists:
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3/
// If it does it's changed to:
// * [`2.0.0-rc2...2.0.0-rc3`](https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3)
r =
/(https:\/\/github\.com\/arduino\/arduino-ide\/compare\/([^\/]*))\/?\s?/gm;
if (r.test(line)) {
return line.replace(r, '[`$2`]($1)');
}
// If nothing matches just return the line as is
return line;
};

View File

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

View File

@@ -4,69 +4,117 @@
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
(() => {
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
const [DEFAULT_ALS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) return undefined;
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const { arduino } = pkg;
if (!arduino) return undefined;
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
choices: ['nightly'],
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
})
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['snapshot_20210124'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
})
.version(false).parse();
const { languageServer, clangd } = arduino;
if (!languageServer) return undefined;
if (!clangd) return undefined;
const alsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version']
const force = yargs['force-download'];
const { platform, arch } = process;
return [languageServer.version, clangd.version];
})();
const build = path.join(__dirname, '..', 'build');
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
if (!DEFAULT_ALS_VERSION) {
shell.echo(
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
);
shell.exit(1);
}
let clangdExecutablePath, lsSuffix, clangdPrefix;
switch (platform) {
case 'darwin':
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'macOS_amd64.zip';
clangdPrefix = 'mac';
break;
case 'linux':
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'Linux_amd64.zip';
clangdPrefix = 'linux'
break;
case 'win32':
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
lsSuffix = 'Windows_amd64.zip';
clangdPrefix = 'windows';
break;
if (!DEFAULT_CLANGD_VERSION) {
shell.echo(
`Could not retrieve clangd version info from the 'package.json'.`
);
shell.exit(1);
}
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`,
})
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: [DEFAULT_CLANGD_VERSION, 'snapshot_20210124'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`,
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`,
})
.version(false)
.parse();
const alsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version'];
const force = yargs['force-download'];
const { platform, arch } = process;
const platformArch = platform + '-' + arch;
const build = path.join(__dirname, '..', 'build');
const lsExecutablePath = path.join(
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
}
if (!lsSuffix || !clangdSuffix) {
shell.echo(
`The arduino-language-server is not available for ${platform} ${arch}.`
);
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${
alsVersion === 'nightly'
? 'nightly/arduino-language-server'
: 'arduino-language-server_' + alsVersion
}_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
clangFormatExecutablePath,
force,
{
strip: 1,
}
if (!lsSuffix) {
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, { strip: 1 }); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
);
})();

View File

@@ -5,16 +5,17 @@ const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untargz = require('decompress-targz');
const untarbz2 = require('decompress-tarbz2');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
shell.echo(String(reason));
shell.exit(1);
throw reason;
});
process.on('uncaughtException', error => {
shell.echo(String(error));
shell.exit(1);
throw error;
process.on('uncaughtException', (error) => {
shell.echo(String(error));
shell.exit(1);
throw error;
});
/**
@@ -23,55 +24,62 @@ process.on('uncaughtException', error => {
* @param filePrefix {string} Prefix of the file name found in the archive
* @param force {boolean} Whether to download even if the target file exists. `false` by default.
*/
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
exports.downloadUnzipFile = async (
url,
targetFile,
filePrefix,
force = false
) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
shell.echo('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [unzip(), untargz(), untarbz2()],
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
const fileIndex = files.findIndex((f) => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(
`The downloaded artifact does not contain any file with prefix ${filePrefix}.`
);
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}
if (
shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile)
.code !== 0
) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
};
/**
* @param url {string} Download URL
@@ -79,42 +87,45 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressOptions = undefined) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(targetDir)) {
if (shell.mkdir('-p', targetDir).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
exports.downloadUnzipAll = async (
url,
targetDir,
targetFile,
force,
decompressOptions = undefined
) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(targetDir)) {
if (shell.mkdir('-p', targetDir).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
let options = {
plugins: [
unzip(),
untargz()
]
};
if (decompressOptions) {
options = Object.assign(options, decompressOptions)
}
const files = await decompress(data, targetDir, options);
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
shell.echo('>>> Decompressing...');
let options = {
plugins: [unzip(), untargz(), untarbz2()],
};
if (decompressOptions) {
options = Object.assign(options, decompressOptions);
}
const files = await decompress(data, targetDir, options);
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
};

View File

@@ -1,20 +1,37 @@
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import {
BoardsService,
SketchesService,
ExecutableService,
Sketch,
LibraryService,
ArduinoDaemon,
} from '../common/protocol';
import { Mutex } from 'async-mutex';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
SelectionService,
ILogger,
DisposableCollection,
} from '@theia/core';
import {
ContextMenuRenderer,
Dialog,
FrontendApplication,
FrontendApplicationContribution,
OpenerService,
LocalStorageService,
OnWillStopAction,
SaveableWidget,
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
@@ -29,55 +46,39 @@ import {
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import {
EditorCommands,
EditorMainMenu,
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager';
import {
BoardsService,
CoreService,
Port,
SketchesService,
ExecutableService,
Sketch,
} from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsDataStore } from './boards/boards-data-store';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { EditorMode } from './editor-mode';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ResponseService } from '../common/protocol/response-service';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
import { IDEUpdater } from '../common/protocol/ide-updater';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { HostedPluginEvents } from './hosted-plugin-events';
const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
export const SKIP_IDE_VERSION = 'skipIDEVersion';
@injectable()
export class ArduinoFrontendContribution
@@ -89,115 +90,70 @@ export class ArduinoFrontendContribution
ColorContribution
{
@inject(ILogger)
protected logger: ILogger;
private readonly logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
private readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
private readonly boardsService: BoardsService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(LibraryService)
private readonly libraryService: LibraryService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
private readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
private readonly editorManager: EditorManager;
@inject(FileService)
protected readonly fileService: FileService;
private readonly fileService: FileService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
private readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly boardsConfigDialog: BoardsConfigDialog;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SketchbookWidgetContribution)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
private readonly statusBar: StatusBar;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
private readonly editorMode: EditorMode;
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
protected executableService: ExecutableService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
private readonly executableService: ExecutableService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
private readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
private readonly appStateService: FrontendApplicationStateService;
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(FileSystemFrontendContribution)
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
protected invalidConfigPopup:
| Promise<void | 'No' | 'Yes' | undefined>
@@ -206,10 +162,34 @@ export class ArduinoFrontendContribution
@postConstruct()
protected async init(): Promise<void> {
const isFirstStartup = !(await this.localStorageService.getData(
INIT_LIBS_AND_PACKAGES
));
if (isFirstStartup) {
await this.localStorageService.setData(INIT_LIBS_AND_PACKAGES, true);
const avrPackage = await this.boardsService.getBoardPackage({
id: 'arduino:avr',
});
const builtInLibrary = (
await this.libraryService.search({
query: 'Arduino_BuiltIn',
})
)[0];
!!avrPackage && (await this.boardsService.install({ item: avrPackage }));
!!builtInLibrary &&
(await this.libraryService.install({
item: builtInLibrary,
installDependencies: true,
}));
}
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
nls.localize(
'arduino/common/offlineIndicator',
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
)
);
}
const updateStatusBar = ({
@@ -220,15 +200,22 @@ export class ArduinoFrontendContribution
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
? `$(microchip) ${selectedBoard.name}`
: '$(close) no board selected',
: `$(close) ${nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
)}`,
className: 'arduino-selected-board',
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? `on ${Port.toString(selectedPort)}`
: '[not connected]',
? nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
)
: nls.localize('arduino/common/notConnected', '[not connected]'),
className: 'arduino-selected-port',
});
}
@@ -237,7 +224,10 @@ export class ArduinoFrontendContribution
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(sketch))) {
if (
CurrentSketch.isValid(sketch) &&
!(await this.sketchService.isTemp(sketch))
) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
@@ -262,21 +252,31 @@ export class ArduinoFrontendContribution
});
}
onStart(app: FrontendApplication): void {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
async onStart(app: FrontendApplication): Promise<void> {
this.updater
.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorageService.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
})
.catch((e) => {
this.messageService.error(
nls.localize(
'arduino/ide-updater/errorCheckingForUpdates',
'Error while checking for Arduino IDE updates.\n{0}',
e.message
)
);
});
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
@@ -286,12 +286,32 @@ export class ArduinoFrontendContribution
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceClientImpl.boardsConfig)
);
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
);
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.language.log' &&
event.newValue !== event.oldValue
) {
start(this.boardsServiceClientImpl.boardsConfig);
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'arduino.language.log':
start(this.boardsServiceClientImpl.boardsConfig);
break;
case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
break;
case 'arduino.ide.updateChannel':
case 'arduino.ide.updateBaseUrl':
this.updater.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
);
break;
}
}
});
this.arduinoPreferences.ready.then(() => {
@@ -299,17 +319,21 @@ export class ArduinoFrontendContribution
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.window.zoomLevel' &&
typeof event.newValue === 'number' &&
event.newValue !== event.oldValue
) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
this.fileSystemFrontendContribution.onDidChangeEditorFile(
({ type, editor }) => {
if (type === FileChangeType.DELETED) {
const editorWidget = editor;
if (SaveableWidget.is(editorWidget)) {
editorWidget.closeWithoutSaving();
} else {
editorWidget.close();
}
}
}
});
app.shell.leftPanelHandler.removeMenu('settings-menu');
);
}
onStop(): void {
@@ -322,9 +346,13 @@ export class ArduinoFrontendContribution
fqbn: string,
name: string | undefined
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
return;
}
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
@@ -359,21 +387,18 @@ export class ArduinoFrontendContribution
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
if (CurrentSketch.isValid(currentSketch)) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
const { clangdUri, lsUri } = await this.executableService.list();
const [clangdPath, lsPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(
new URI(await this.configService.getCliConfigFileUri())
),
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
@@ -385,10 +410,10 @@ export class ArduinoFrontendContribution
'arduino.languageserver.start',
{
lsPath,
cliPath,
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
cliDaemonInstance: '1',
board: {
fqbn,
name: name ? `"${name}"` : undefined,
@@ -421,7 +446,7 @@ export class ArduinoFrontendContribution
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor',
tooltip: nls.localize('arduino/common/serialMonitor', 'Serial Monitor'),
});
}
@@ -443,9 +468,21 @@ export class ArduinoFrontendContribution
}
},
});
for (const command of [
EditorCommands.SPLIT_EDITOR_DOWN,
EditorCommands.SPLIT_EDITOR_LEFT,
EditorCommands.SPLIT_EDITOR_RIGHT,
EditorCommands.SPLIT_EDITOR_UP,
EditorCommands.SPLIT_EDITOR_VERTICAL,
EditorCommands.SPLIT_EDITOR_HORIZONTAL,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR,
]) {
registry.unregisterCommand(command);
}
}
registerMenus(registry: MenuModelRegistry) {
registerMenus(registry: MenuModelRegistry): void {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
@@ -456,12 +493,21 @@ export class ArduinoFrontendContribution
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerSubmenu(
ArduinoMenus.SKETCH,
nls.localize('arduino/menu/sketch', 'Sketch')
);
registry.registerSubmenu(
ArduinoMenus.TOOLS,
nls.localize('arduino/menu/tools', 'Tools')
);
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4',
label: nls.localize(
'arduino/debug/optimizeForDebugging',
'Optimize for Debugging'
),
order: '5',
});
}
@@ -472,13 +518,17 @@ export class ArduinoFrontendContribution
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
const message = nls.localize(
'arduino/common/oldFormat',
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
sketch.name
);
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
this.messageService
.info(message, 'Later', 'Yes')
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
.then(async (answer) => {
if (answer === 'Yes') {
if (answer === yes) {
this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
@@ -501,12 +551,19 @@ export class ArduinoFrontendContribution
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
): Promise<unknown> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
return this.editorManager.open(
new URI(uri),
options ?? {
mode: 'reveal',
preview: false,
counter: 0,
}
);
}
}
@@ -590,4 +647,57 @@ export class ArduinoFrontendContribution
}
);
}
onWillStop(): OnWillStopAction {
return {
reason: 'temp-sketch',
action: () => {
return this.showTempSketchDialog();
},
};
}
private async showTempSketchDialog(): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return true;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp) {
return true;
}
const messageBoxResult = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'arduino/sketch/saveTempSketch',
'Save your sketch to open it again later.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [
Dialog.CANCEL,
nls.localizeByDefault('Save As...'),
nls.localizeByDefault("Don't Save"),
],
}
);
const result = messageBoxResult.response;
if (result === 2) {
return true;
} else if (result === 1) {
return !!(await this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: false,
wipeOriginal: true,
}
));
}
return false;
}
}

View File

@@ -1,5 +1,5 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from 'inversify';
import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -42,15 +42,14 @@ import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@th
import { KeymapsFrontendContribution } from './theia/keymaps/keymaps-frontend-contribution';
import { KeymapsFrontendContribution as TheiaKeymapsFrontendContribution } from '@theia/keymaps/lib/browser/keymaps-frontend-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { EditorPreviewContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
@@ -69,20 +68,12 @@ import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import {
MonitorServicePath,
MonitorService,
MonitorServiceClient,
} from '../common/protocol/monitor-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorModel } from './monitor/monitor-model';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
@@ -138,7 +129,6 @@ import { PreferencesContribution } from './theia/preferences/preferences-contrib
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { KeybindingRegistry } from './theia/core/keybindings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
@@ -154,19 +144,27 @@ import {
} from '../common/protocol/examples-service';
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
import { IncludeLibrary } from './contributions/include-library';
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { OutputChannelManager } from './theia/output/output-channel';
import {
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
OutputChannelRegistryMainImpl,
} from './theia/plugin-ext/output-channel-registry-main';
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import {
ExecutableService,
ExecutableServicePath,
MonitorManagerProxy,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
MonitorManagerProxyPath,
} from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
import {
ResponseServicePath,
ResponseService,
ResponseServiceArduino,
ResponseServicePath,
} from '../common/protocol/response-service';
import { NotificationCenter } from './notification-center';
import {
@@ -189,12 +187,12 @@ import { BoardSelection } from './contributions/board-selection';
import { OpenRecentSketch } from './contributions/open-recent-sketch';
import { Help } from './contributions/help';
import { bindArduinoPreferences } from './arduino-preferences';
import { SettingsService } from './dialogs/settings/settings';
import {
SettingsService,
SettingsDialog,
SettingsWidget,
SettingsDialogProps,
} from './settings';
} from './dialogs/settings/settings-dialog';
import { AddFile } from './contributions/add-file';
import { ArchiveSketch } from './contributions/archive-sketch';
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
@@ -206,12 +204,12 @@ import { DebugConfigurationManager } from './theia/debug/debug-configuration-man
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 { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { DebugEditorModel } from './theia/debug/debug-editor-model';
import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model';
import { StorageWrapper } from './storage-wrapper';
import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@@ -223,7 +221,7 @@ import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbo
import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container';
import { CreateApi } from './create/create-api';
import { ShareSketchDialog } from './dialogs.ts/cloud-share-sketch-dialog';
import { ShareSketchDialog } from './dialogs/cloud-share-sketch-dialog';
import { AuthenticationClientService } from './auth/authentication-client-service';
import {
AuthenticationService,
@@ -237,20 +235,78 @@ import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
import { UploadFirmware } from './contributions/upload-firmware';
import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
const ElementQueries = require('css-element-queries/src/ElementQueries');
import { UploadCertificate } from './contributions/upload-certificate';
import {
ArduinoFirmwareUploader,
ArduinoFirmwareUploaderPath,
} from '../common/protocol/arduino-firmware-uploader';
import {
UploadCertificateDialog,
UploadCertificateDialogProps,
UploadCertificateDialogWidget,
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution';
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';
import {
IDEUpdater,
IDEUpdaterClient,
IDEUpdaterPath,
} from '../common/protocol/ide-updater';
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';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
MonacoThemingService.register({
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/arduino.color-theme.json'),
json: require('../../src/browser/data/default.color-theme.json'),
});
MonacoThemingService.register({
id: 'arduino-theme-dark',
label: 'Dark (Arduino)',
uiTheme: 'vs-dark',
json: require('../../src/browser/data/dark.color-theme.json'),
});
export default new ContainerModule((bind, unbind, isBound, rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
@@ -355,7 +411,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: 'Select Board',
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
});
// Core service
@@ -369,36 +425,43 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
// Serial monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
},
}));
// Frontend binding for the serial monitor service
bind(MonitorService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client);
})
.inSingletonScope();
bind(MonitorConnection).toSelf().inSingletonScope();
// Serial monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient)
.toDynamicValue((context) => {
const client = context.container.get(MonitorServiceClientImpl);
bind(MonitorManagerProxyFactory).toFactory(
(context) => () =>
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
);
bind(MonitorManagerProxy)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
MonitorServicePath,
client
);
return client;
})
MonitorManagerProxyPath,
context.container.get(MonitorManagerProxyClient)
)
)
.inSingletonScope();
// Monitor manager proxy client to receive and delegate pluggable monitors
// notifications from the backend
bind(MonitorManagerProxyClient)
.to(MonitorManagerProxyClientImpl)
.inSingletonScope();
bind(WorkspaceService).toSelf().inSingletonScope();
@@ -423,7 +486,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaKeymapsFrontendContribution)
.to(KeymapsFrontendContribution)
.inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaEditorPreviewContribution)
.to(EditorPreviewContribution)
.inSingletonScope();
rebind(TheiaMonacoStatusBarContribution)
.to(MonacoStatusBarContribution)
.inSingletonScope();
@@ -445,7 +510,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaPreferencesContribution)
.to(PreferencesContribution)
.inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution)
.to(WorkspaceCommandContribution)
.inSingletonScope();
@@ -476,6 +540,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).to(EditorManager);
// replace search icon
rebind(TheiaSearchInWorkspaceFactory)
.to(SearchInWorkspaceFactory)
.inSingletonScope();
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
({ container }) => {
const childContainer = createTreeContainer(container);
@@ -522,6 +596,21 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
.inSingletonScope();
bind(Formatter)
.toDynamicValue(({ container }) =>
WebSocketConnectionProvider.createProxy(container, FormatterPath)
)
.inSingletonScope();
bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
ArduinoFirmwareUploaderPath
)
)
.inSingletonScope();
// File-system extension
bind(FileSystemExt)
.toDynamicValue((context) =>
@@ -571,12 +660,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, About);
Contribution.configure(bind, Debug);
Contribution.configure(bind, Sketchbook);
Contribution.configure(bind, UploadFirmware);
Contribution.configure(bind, UploadCertificate);
Contribution.configure(bind, BoardSelection);
Contribution.configure(bind, OpenRecentSketch);
Contribution.configure(bind, Help);
Contribution.configure(bind, AddFile);
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoFormattingConflictsContribution).toService(
MonacoFormattingConflictsContribution
);
bind(ResponseServiceImpl)
.toSelf()
@@ -589,7 +689,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
return responseService;
});
bind(ResponseService).toService(ResponseServiceImpl);
bind(ResponseServiceArduino).toService(ResponseServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);
@@ -620,6 +722,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
@@ -627,6 +731,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
// Do not fetch the `catalog.json` from Azure on FE load.
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
rebind(TheiaDefaultJsonSchemaContribution).toService(
DefaultJsonSchemaContribution
);
// Do not block the app startup when initializing the editor navigation history.
bind(EditorNavigationContribution).toSelf().inSingletonScope();
rebind(TheiaEditorNavigationContribution).toService(
EditorNavigationContribution
);
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
@@ -639,16 +763,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// Patch for the debug hover: https://github.com/eclipse-theia/theia/pull/9256/
rebind(DebugEditorModelFactory)
.toDynamicValue(
({ container }) =>
<DebugEditorModelFactory>(
((editor) => DebugEditorModel.createModel(container, editor))
)
)
.inSingletonScope();
// Preferences
bindArduinoPreferences(bind);
@@ -658,7 +772,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SettingsWidget).toSelf().inSingletonScope();
bind(SettingsDialog).toSelf().inSingletonScope();
bind(SettingsDialogProps).toConstantValue({
title: 'Preferences',
title: nls.localize(
'vscode/preferences.contribution/preferences',
'Preferences'
),
});
bind(StorageWrapper).toSelf().inSingletonScope();
@@ -713,4 +830,49 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
}));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware',
});
bind(UploadCertificateDialogWidget).toSelf().inSingletonScope();
bind(UploadCertificateDialog).toSelf().inSingletonScope();
bind(UploadCertificateDialogProps).toConstantValue({
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',
});
bind(IDEUpdaterCommands).toSelf().inSingletonScope();
bind(CommandContribution).toService(IDEUpdaterCommands);
// Frontend binding for the IDE Updater service
bind(IDEUpdaterClientImpl).toSelf().inSingletonScope();
bind(IDEUpdaterClient).toService(IDEUpdaterClientImpl);
bind(IDEUpdater)
.toDynamicValue((context) => {
const client = context.container.get(IDEUpdaterClientImpl);
return ElectronIpcConnectionProvider.createProxy(
context.container,
IDEUpdaterPath,
client
);
})
.inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
});

View File

@@ -1,4 +1,4 @@
import { interfaces } from 'inversify';
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
@@ -6,31 +6,47 @@ import {
PreferenceContribution,
PreferenceSchema,
} from '@theia/core/lib/browser/preferences';
import { nls } from '@theia/core/lib/common';
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
export enum UpdateChannel {
Stable = 'stable',
Nightly = 'nightly',
}
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
properties: {
'arduino.language.log': {
type: 'boolean',
description:
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
description: nls.localize(
'arduino/preferences/language.log',
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default."
),
default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: 'True for verbose compile output. False by default',
description: nls.localize(
'arduino/preferences/compile.verbose',
'True for verbose compile output. False by default'
),
default: false,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description:
"Tells gcc which warning level to use. It's 'None' by default",
description: nls.localize(
'arduino/preferences/compile.warnings',
"Tells gcc which warning level to use. It's 'None' by default"
),
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: 'True for verbose upload output. False by default.',
description: nls.localize(
'arduino/preferences/upload.verbose',
'True for verbose upload output. False by default.'
),
default: false,
},
'arduino.upload.verify': {
@@ -39,76 +55,123 @@ export const ArduinoConfigSchema: PreferenceSchema = {
},
'arduino.window.autoScale': {
type: 'boolean',
description:
'True if the user interface automatically scales with the font size.',
description: nls.localize(
'arduino/preferences/window.autoScale',
'True if the user interface automatically scales with the font size.'
),
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description:
'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: 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.'
),
default: 0,
},
'arduino.ide.autoUpdate': {
type: 'boolean',
description:
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
'arduino.ide.updateChannel': {
type: 'string',
enum: Object.values(UpdateChannel) as UpdateChannel[],
default: UpdateChannel.Stable,
description: nls.localize(
'arduino/preferences/ide.updateChannel',
"Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build."
),
},
'arduino.ide.updateBaseUrl': {
type: 'string',
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
type: 'string',
description: nls.localize(
'arduino/preferences/board.certificates',
'List of certificates that can be uploaded to boards'
),
default: '',
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
description: nls.localize(
'arduino/preferences/sketchbook.showAllFiles',
'True to show all sketch files inside the sketch. It is false by default.'
),
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
description: nls.localize(
'arduino/preferences/cloud.enabled',
'True if the sketch sync functions are enabled. Defaults to true.'
),
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
description: nls.localize(
'arduino/preferences/cloud.pull.warn',
'True if users should be warned before pulling a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
description: nls.localize(
'arduino/preferences/cloud.push.warn',
'True if users should be warned before pushing a cloud sketch. Defaults to true.'
),
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
description: nls.localize(
'arduino/preferences/cloud.pushpublic.warn',
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.'
),
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
description: nls.localize(
'arduino/preferences/cloud.sketchSyncEnpoint',
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
),
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
description: nls.localize(
'arduino/preferences/auth.clientID',
'The OAuth2 client ID.'
),
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
description: nls.localize(
'arduino/preferences/auth.domain',
'The OAuth2 domain.'
),
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
description: nls.localize(
'arduino/preferences/auth.audience',
'The OAuth2 audience.'
),
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
description: nls.localize(
'arduino/preferences/auth.registerUri',
'The URI used to register a new user.'
),
default: 'https://auth.arduino.cc/login#/register',
},
},
@@ -122,7 +185,9 @@ export interface ArduinoConfiguration {
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
'arduino.board.certificates': string;
'arduino.sketchbook.showAllFiles': boolean;
'arduino.cloud.enabled': boolean;
'arduino.cloud.pull.warn': boolean;
@@ -138,16 +203,10 @@ export interface ArduinoConfiguration {
export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences(
preferences: PreferenceService
): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema);
}
export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue((ctx) => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
return createArduinoPreferences(preferences);
return createPreferenceProxy(preferences, ArduinoConfigSchema);
});
bind(PreferenceContribution).toConstantValue({
schema: ArduinoConfigSchema,

View File

@@ -1,5 +1,4 @@
import { toUnix } from 'upath';
import URI from '@theia/core/lib/common/uri';
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';
@@ -61,12 +60,8 @@ export class ArduinoWorkspaceRootResolver {
// - 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 = hash.slice(1); // Trim the leading `#`.
return new URI(
toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))
)
.withScheme('file')
.toString();
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

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

View File

@@ -1,15 +1,21 @@
import { Command } from '@theia/core/lib/common/command';
export namespace CloudUserCommands {
export const LOGIN: Command = {
id: 'arduino-cloud--login',
label: 'Sign in',
};
export const LOGIN = Command.toLocalizedCommand(
{
id: 'arduino-cloud--login',
label: 'Sign in',
},
'arduino/cloud/signIn'
);
export const LOGOUT: Command = {
id: 'arduino-cloud--logout',
label: 'Sign Out',
};
export const LOGOUT = Command.toLocalizedCommand(
{
id: 'arduino-cloud--logout',
label: 'Sign Out',
},
'arduino/cloud/signOut'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
@@ -7,10 +7,10 @@ import {
Board,
} from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { BoardsConfig } from './boards-config';
import { Installable } from '../../common/protocol';
import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, ResponseServiceArduino } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { nls } from '@theia/core/lib/common';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
@@ -27,8 +27,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
@@ -44,9 +44,10 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
const { selectedBoard, selectedPort } = config;
if (
selectedBoard &&
selectedPort &&
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
) {
this.notifications.push(selectedBoard);
@@ -81,12 +82,23 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
const version = candidate.availableVersions[0]
? `[v ${candidate.availableVersions[0]}]`
: '';
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const manualInstall = nls.localize(
'arduino/board/installManually',
'Install Manually'
);
// tslint:disable-next-line:max-line-length
this.messageService
.info(
`The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`,
'Install Manually',
'Yes'
nls.localize(
'arduino/board/installNow',
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
candidate.name,
version,
selectedBoard.name
),
manualInstall,
yes
)
.then(async (answer) => {
const index = this.notifications.findIndex((board) =>
@@ -95,7 +107,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
if (index !== -1) {
this.notifications.splice(index, 1);
}
if (answer === 'Yes') {
if (answer === yes) {
await Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
@@ -105,7 +117,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
});
return;
}
if (answer) {
if (answer === manualInstall) {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>

View File

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

View File

@@ -1,15 +1,12 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import {
AbstractDialog,
DialogProps,
Widget,
DialogError,
} from '@theia/core/lib/browser';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {}
@@ -29,15 +26,17 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
constructor(
@inject(BoardsConfigDialogProps)
protected readonly props: BoardsConfigDialogProps
protected override readonly props: BoardsConfigDialogProps
) {
super(props);
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
@postConstruct()
@@ -53,7 +52,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
async open(
override async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
@@ -67,7 +66,10 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.textContent = nls.localize(
'arduino/board/configDialogTitle',
'Select Other Board & Port'
);
title.classList.add('title');
head.appendChild(title);
@@ -76,8 +78,14 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.',
nls.localize(
'arduino/board/configDialog1',
'Select both a Board and a Port if you want to upload a sketch.'
),
nls.localize(
'arduino/board/configDialog2',
'If you only select a Board you will be able to compile, but not to upload your sketch.'
),
]) {
const p = document.createElement('div');
p.textContent = paragraph;
@@ -87,7 +95,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
return head;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -102,26 +110,29 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
this.update();
}
protected onUpdateRequest(msg: Message) {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return 'Please pick a board connected to the port you have selected.';
return nls.localize(
'arduino/board/pleasePickBoard',
'Please pick a board connected to the port you have selected.'
);
}
return false;
}

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
@@ -10,7 +10,13 @@ import {
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
import {
AvailableBoard,
BoardsServiceProvider,
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
@@ -24,6 +30,7 @@ export namespace BoardsConfig {
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
@@ -42,7 +49,7 @@ export abstract class Item<T> extends React.Component<{
missing?: boolean;
details?: string;
}> {
render(): React.ReactNode {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
@@ -94,14 +101,18 @@ export class BoardsConfig extends React.Component<
};
}
componentDidMount() {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange((state) => {
if (state === 'ready') {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
}
}),
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
this.updatePorts(
event.newState.ports,
@@ -136,11 +147,11 @@ export class BoardsConfig extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
@@ -162,7 +173,7 @@ export class BoardsConfig extends React.Component<
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some((port) => Port.equals(port, selectedPort))) {
if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
@@ -183,11 +194,50 @@ export class BoardsConfig extends React.Component<
.filter(notEmpty);
}
protected get availableBoards(): AvailableBoard[] {
return this.props.boardsServiceProvider.availableBoards;
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
// Available ports must be sorted in this order:
// 1. Serial with recognized boards
// 2. Serial with guessed boards
// 3. Serial with incomplete boards
// 4. Network with recognized boards
// 5. Other protocols with recognized boards
const ports = (await availablePorts).sort((left: Port, right: Port) => {
if (left.protocol === 'serial' && right.protocol !== 'serial') {
return -1;
} else if (left.protocol !== 'serial' && right.protocol === 'serial') {
return 1;
} else if (left.protocol === 'network' && right.protocol !== 'network') {
return -1;
} else if (left.protocol !== 'network' && right.protocol === 'network') {
return 1;
} else if (left.protocol === right.protocol) {
// We show ports, including those that have guessed
// or unrecognized boards, so we must sort those too.
const leftBoard = this.availableBoards.find(
(board) => board.port === left
);
const rightBoard = this.availableBoards.find(
(board) => board.port === right
);
if (leftBoard && !rightBoard) {
return -1;
} else if (!leftBoard && rightBoard) {
return 1;
} else if (leftBoard?.state! < rightBoard?.state!) {
return -1;
} else if (leftBoard?.state! > rightBoard?.state!) {
return 1;
}
}
return naturalCompare(left.address, right.address);
});
return { knownPorts: ports };
};
protected toggleFilterPorts = () => {
@@ -206,7 +256,7 @@ export class BoardsConfig extends React.Component<
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}
@@ -265,7 +315,7 @@ export class BoardsConfig extends React.Component<
<div className="boards list">
{Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={`${board.name}-${board.packageName}`}
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
@@ -280,18 +330,34 @@ export class BoardsConfig extends React.Component<
}
protected renderPorts(): React.ReactNode {
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
const ports = this.state.knownPorts.filter(filter);
let ports = [] as Port[];
if (this.state.showAllPorts) {
ports = this.state.knownPorts;
} else {
ports = this.state.knownPorts.filter((port) => {
if (port.protocol === 'serial') {
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of this.availableBoards) {
if (board.port?.address === port.address) {
return true;
}
}
});
}
return !ports.length ? (
<div className="loading noselect">No ports discovered</div>
) : (
<div className="ports list">
{ports.map((port) => (
<Item<Port>
key={Port.toString(port)}
key={`${port.id}`}
item={port}
label={Port.toString(port)}
selected={Port.equals(this.state.selectedPort, port)}
selected={Port.sameAs(this.state.selectedPort, port)}
onClick={this.selectPort}
/>
))}
@@ -302,7 +368,12 @@ export class BoardsConfig extends React.Component<
protected renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label title="Shows all available ports when enabled">
<label
title={nls.localize(
'arduino/board/showAllAvailablePorts',
'Shows all available ports when enabled'
)}
>
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
@@ -345,7 +416,7 @@ export namespace BoardsConfig {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
export function setConfig(

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -12,6 +12,8 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@@ -30,11 +32,20 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.appStateService
.reachedState('ready')
.then(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsDataStore.onChanged(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
@@ -115,9 +126,13 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
'z02_programmers',
];
const programmerNls = nls.localize(
'arduino/board/programmer',
'Programmer'
);
const label = selectedProgrammer
? `Programmer: "${selectedProgrammer.name}"`
: 'Programmer';
? `${programmerNls}: "${selectedProgrammer.name}"`
: programmerNls;
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(
Disposable.create(() =>

View File

@@ -1,7 +1,6 @@
import { injectable, inject, named } from 'inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import {
FrontendApplicationContribution,
@@ -11,7 +10,6 @@ import { notEmpty } from '../../common/utils';
import {
BoardsService,
ConfigOption,
Installable,
BoardDetails,
Programmer,
} from '../../common/protocol';
@@ -36,16 +34,12 @@ export class BoardsDataStore implements FrontendApplicationContribution {
onStart(): void {
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
const { installedVersion: version } = item;
if (!version) {
return;
}
let shouldFireChanged = false;
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty)
.filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<
ConfigOption[] | undefined
>(key);
@@ -72,33 +66,20 @@ export class BoardsDataStore implements FrontendApplicationContribution {
async appendConfigToFqbn(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
const { configOptions } = await this.getData(fqbn);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<BoardsDataStore.Data> {
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const version = await boardsPackageVersion;
if (!version) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn, version);
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<
BoardsDataStore.Data | undefined
>(key, undefined);
@@ -124,25 +105,16 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const data = deepClone(await this.getData(fqbn));
const { programmers } = data;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({
fqbn,
data: { ...data, selectedProgrammer },
version,
});
this.fireChanged();
return true;
@@ -153,12 +125,9 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
}: { fqbn: string; option: string; selectedValue: string }
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
@@ -176,12 +145,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
if (!updated) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
await this.setData({ fqbn, data });
this.fireChanged();
return true;
}
@@ -189,18 +153,16 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected async setData({
fqbn,
data,
version,
}: {
fqbn: string;
data: BoardsDataStore.Data;
version: Installable.Version;
}): Promise<void> {
const key = this.getStorageKey(fqbn, version);
const key = this.getStorageKey(fqbn);
return this.storageService.setData(key, data);
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
protected getStorageKey(fqbn: string): string {
return `.arduinoIDE-configOptions-${fqbn}`;
}
protected async getBoardDetailsSafe(
@@ -231,21 +193,6 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(
fqbn: string | undefined
): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
const boardsPackage = await this.boardsService.getContainerBoardPackage({
fqbn,
});
if (!boardsPackage) {
return undefined;
}
return boardsPackage.installedVersion;
}
}
export namespace BoardsDataStore {

View File

@@ -1,15 +1,16 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
BoardsPackage,
BoardsService,
} from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsListWidget extends ListWidget<BoardsPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
constructor(
@inject(BoardsService) protected service: BoardsService,
@@ -19,7 +20,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
super({
id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip',
iconClass: 'fa fa-arduino-boards',
searchable: service,
installable: service,
itemLabel: (item: BoardsPackage) => item.name,
@@ -29,7 +30,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
}
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() =>
@@ -41,7 +42,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -52,12 +53,17 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
}): Promise<void> {
await super.install({ item, progressId, version });
this.messageService.info(
`Successfully installed platform ${item.name}:${version}`,
nls.localize(
'arduino/board/succesfullyInstalledPlatform',
'Successfully installed platform {0}:{1}',
item.name,
version
),
{ timeout: 3000 }
);
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {
@@ -66,7 +72,12 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
}): Promise<void> {
await super.uninstall({ item, progressId });
this.messageService.info(
`Successfully uninstalled platform ${item.name}:${item.installedVersion}`,
nls.localize(
'arduino/board/succesfullyUninstalledPlatform',
'Successfully uninstalled platform {0}:{1}',
item.name,
item.installedVersion!
),
{ timeout: 3000 }
);
}

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandService } from '@theia/core/lib/common/command';
@@ -12,12 +12,14 @@ import {
BoardsPackage,
AttachedBoardsChangeEvent,
BoardWithPackage,
BoardUserField,
} from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
import { NotificationCenter } from '../notification-center';
import { ArduinoCommands } from '../arduino-commands';
import { StorageWrapper } from '../storage-wrapper';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution {
@@ -41,6 +43,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -63,11 +66,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
* This even also fires, when the boards package was not available for the currently selected board,
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
* currently selected board.\
* This even also emitted when the board package for the currently selected board was uninstalled.
* This event is also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
readonly onAvailableBoardsChanged =
this.onAvailableBoardsChangedEmitter.event;
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
onStart(): void {
this.notificationCenter.onAttachedBoardsChanged(
@@ -87,6 +91,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
]).then(([attachedBoards, availablePorts]) => {
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
});
}
@@ -101,6 +106,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
this._attachedBoards = event.newState.boards;
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
@@ -134,14 +140,20 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
selectedBoard.packageId === event.item.id &&
!installedBoard
) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
this.messageService
.warn(
`Could not find previously selected board '${selectedBoard.name}' in installed platform '${event.item.name}'. Please manually reselect the board you want to use. Do you want to reselect it now?`,
'Reselect later',
'Yes'
nls.localize(
'arduino/board/couldNotFindPreviouslySelected',
"Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?",
selectedBoard.name,
event.item.name
),
nls.localize('arduino/board/reselectLater', 'Reselect later'),
yes
)
.then(async (answer) => {
if (answer === 'Yes') {
if (answer === yes) {
this.commandService.executeCommand(
ArduinoCommands.OPEN_BOARDS_DIALOG.id,
selectedBoard.name
@@ -173,8 +185,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
? selectedBoard
: this._availableBoards.find((availableBoard) =>
Board.sameAs(availableBoard, selectedBoard)
);
Board.sameAs(availableBoard, selectedBoard)
);
if (
selectedAvailableBoard &&
selectedAvailableBoard.selected &&
@@ -218,7 +230,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
)) {
if (
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
this.latestValidBoardsConfig.selectedBoard.name === board.name
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
this.latestValidBoardsConfig.selectedPort.protocol === board.port?.protocol
) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
@@ -232,7 +245,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
set boardsConfig(config: BoardsConfig.Config) {
this.doSetBoardsConfig(config);
this.setBoardsConfig(config);
this.saveState().finally(() =>
this.reconcileAvailableBoards().finally(() =>
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)
@@ -244,7 +257,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return this._boardsConfig;
}
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
protected setBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.latestBoardsConfig = this._boardsConfig;
@@ -264,6 +277,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return boards;
}
async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
return [];
}
const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) {
return [];
}
const protocol = this._boardsConfig.selectedPort.protocol;
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
}
/**
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
*/
@@ -277,9 +302,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
if (!config.selectedBoard) {
if (!options.silent) {
this.messageService.warn('No boards selected.', {
timeout: 3000,
});
this.messageService.warn(
nls.localize('arduino/board/noneSelected', 'No boards selected.'),
{
timeout: 3000,
}
);
}
return false;
}
@@ -301,9 +329,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const { name } = config.selectedBoard;
if (!config.selectedPort) {
if (!options.silent) {
this.messageService.warn(`No ports selected for board: '${name}'.`, {
timeout: 3000,
});
this.messageService.warn(
nls.localize(
'arduino/board/noPortsSelected',
"No ports selected for board: '{0}'.",
name
),
{
timeout: 3000,
}
);
}
return false;
}
@@ -311,7 +346,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
if (!config.selectedBoard.fqbn) {
if (!options.silent) {
this.messageService.warn(
`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`,
nls.localize(
'arduino/board/noFQBN',
'The FQBN is not available for the selected board "{0}". Do you have the corresponding core installed?',
name
),
{ timeout: 3000 }
);
}
@@ -332,19 +371,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) =>
haystack.find(
(board) =>
Board.equals(needle, board) && Port.equals(needle.port, board.port)
Board.equals(needle, board) && Port.sameAs(needle.port, board.port)
);
const timeoutTask =
!!timeout && timeout > 0
? new Promise<void>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
)
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
)
)
: new Promise<void>(() => {
/* never */
});
/* never */
});
const waitUntilTask = new Promise<void>((resolve) => {
let candidate = find(what, this.availableBoards);
if (candidate) {
@@ -363,7 +402,6 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
protected async reconcileAvailableBoards(): Promise<void> {
const attachedBoards = this._attachedBoards;
const availablePorts = this._availablePorts;
// Unset the port on the user's config, if it is not available anymore.
if (
@@ -372,7 +410,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
Port.sameAs(port, this.boardsConfig.selectedPort)
)
) {
this.doSetBoardsConfig({
this.setBoardsConfig({
selectedBoard: this.boardsConfig.selectedBoard,
selectedPort: undefined,
});
@@ -381,51 +419,73 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const boardsConfig = this.boardsConfig;
const currentAvailableBoards = this._availableBoards;
const availableBoards: AvailableBoard[] = [];
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
const availableBoardPorts = availablePorts.filter((port) => {
if (port.protocol === 'serial') {
// We always show all serial ports, even if there
// is no recognized board connected to it
return true;
}
for (const boardPort of availableBoardPorts) {
let state = AvailableBoard.State.incomplete; // Initial pessimism.
let board = attachedSerialBoards.find(({ port }) =>
Port.sameAs(boardPort, port)
);
if (board) {
state = AvailableBoard.State.recognized;
} else {
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
boardPort
);
if (lastSelectedBoard) {
board = {
...lastSelectedBoard,
port: boardPort,
};
state = AvailableBoard.State.guessed;
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of attachedBoards) {
if (board.port?.address === port.address) {
return true;
}
}
if (!board) {
availableBoards.push({
name: 'Unknown',
port: boardPort,
state,
});
} else {
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
availableBoards.push({
return false;
});
for (const boardPort of availableBoardPorts) {
const board = attachedBoards.find(({ port }) =>
Port.sameAs(boardPort, port)
);
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
boardPort
);
let availableBoard = {} as AvailableBoard;
if (board) {
availableBoard = {
...board,
state,
selected,
state: AvailableBoard.State.recognized,
selected: BoardsConfig.Config.sameAs(boardsConfig, board),
port: boardPort,
});
};
} else if (lastSelectedBoard) {
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
availableBoard = {
...lastSelectedBoard,
state: AvailableBoard.State.guessed,
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
port: boardPort,
};
} else {
availableBoard = {
name: nls.localize('arduino/common/unknown', 'Unknown'),
port: boardPort,
state: AvailableBoard.State.incomplete,
};
}
availableBoards.push(availableBoard);
}
if (
boardsConfig.selectedBoard &&
!availableBoards.some(({ selected }) => selected)
) {
// 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.
const found = availableBoards.findIndex(
(board) => board.port?.address === boardsConfig.selectedPort?.address
);
if (found >= 0) {
availableBoards.splice(found, 1);
}
availableBoards.push({
...boardsConfig.selectedBoard,
port: boardsConfig.selectedPort,
@@ -434,28 +494,24 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
});
}
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.compare);
let hasChanged =
sortedAvailableBoards.length !== currentAvailableBoards.length;
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
availableBoards.sort(AvailableBoard.compare);
let hasChanged = availableBoards.length !== currentAvailableBoards.length;
for (let i = 0; !hasChanged && i < availableBoards.length; i++) {
const [left, right] = [availableBoards[i], currentAvailableBoards[i]];
hasChanged =
AvailableBoard.compare(
sortedAvailableBoards[i],
currentAvailableBoards[i]
) !== 0;
!!AvailableBoard.compare(left, right) ||
left.selected !== right.selected;
}
if (hasChanged) {
this._availableBoards = sortedAvailableBoards;
this._availableBoards = availableBoards;
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
}
}
protected async getLastSelectedBoardOnPort(
port: Port | string | undefined
port: Port
): Promise<Board | undefined> {
if (!port) {
return undefined;
}
const key = this.getLastSelectedBoardOnPortKey(port);
return this.getData<Board>(key);
}
@@ -478,9 +534,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${
typeof port === 'string' ? port : Port.toString(port)
}`;
return `last-selected-board-on-port:${typeof port === 'string' ? port : port.address
}`;
}
protected async loadState(): Promise<void> {
@@ -564,35 +619,39 @@ export namespace AvailableBoard {
return !!board.port;
}
// Available boards must be sorted in this order:
// 1. Serial with recognized boards
// 2. Serial with guessed boards
// 3. Serial with incomplete boards
// 4. Network with recognized boards
// 5. Other protocols with recognized boards
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
if (left.selected && !right.selected) {
if (left.port?.protocol === 'serial' && right.port?.protocol !== 'serial') {
return -1;
}
if (right.selected && !left.selected) {
} else if (
left.port?.protocol !== 'serial' &&
right.port?.protocol === 'serial'
) {
return 1;
}
let result = naturalCompare(left.name, right.name);
if (result !== 0) {
return result;
}
if (left.fqbn && right.fqbn) {
result = naturalCompare(left.fqbn, right.fqbn);
if (result !== 0) {
return result;
} else if (
left.port?.protocol === 'network' &&
right.port?.protocol !== 'network'
) {
return -1;
} else if (
left.port?.protocol !== 'network' &&
right.port?.protocol === 'network'
) {
return 1;
} else if (left.port?.protocol === right.port?.protocol) {
// We show all ports, including those that have guessed
// or unrecognized boards, so we must sort those too.
if (left.state < right.state) {
return -1;
} else if (left.state > right.state) {
return 1;
}
}
if (left.port && right.port) {
result = Port.compare(left.port, right.port);
if (result !== 0) {
return result;
}
}
if (!!left.selected && !right.selected) {
return -1;
}
if (!!right.selected && !left.selected) {
return 1;
}
return left.state - right.state;
return naturalCompare(left.port?.address!, right.port?.address!);
};
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
@@ -9,6 +9,7 @@ import {
BoardsServiceProvider,
AvailableBoard,
} from './boards-service-provider';
import { nls } from '@theia/core/lib/common';
export interface BoardsDropDownListCoords {
readonly top: number;
@@ -40,7 +41,7 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
}
}
render(): React.ReactNode {
override render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
@@ -49,6 +50,10 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
if (coords === 'hidden') {
return '';
}
const footerLabel = nls.localize(
'arduino/board/openBoardsConfig',
'Select other board and port…'
);
return (
<div
className="arduino-boards-dropdown-list"
@@ -57,17 +62,25 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
...coords,
}}
>
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig(),
})}
{items
.map(({ name, port, selected, onClick }) => ({
label: `${name} at ${Port.toString(port)}`,
label: nls.localize(
'arduino/board/boardListItem',
'{0} at {1}',
name,
Port.toString(port)
),
selected,
onClick,
}))
.map(this.renderItem)}
<div
key={footerLabel}
className="arduino-boards-dropdown-item arduino-board-dropdown-footer"
onClick={() => this.props.openBoardsConfig()}
>
<div>{footerLabel}</div>
</div>
</div>
);
}
@@ -117,13 +130,13 @@ export class BoardsToolBarItem extends React.Component<
});
}
componentDidMount() {
override componentDidMount(): void {
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
@@ -148,11 +161,14 @@ export class BoardsToolBarItem extends React.Component<
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
override render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, {
default: 'no board selected',
default: nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
),
});
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);

View File

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

View File

@@ -0,0 +1,28 @@
import * as React from '@theia/core/shared/react';
export type ProgressBarProps = {
percent?: number;
showPercentage?: boolean;
};
export default function ProgressBar({
percent = 0,
showPercentage = false,
}: ProgressBarProps): React.ReactElement {
const roundedPercent = Math.round(percent);
return (
<div className="progress-bar">
<div className="progress-bar--outer">
<div
className="progress-bar--inner"
style={{ width: `${roundedPercent}%` }}
/>
</div>
{showPercentage && (
<div className="progress-bar--percentage">
<div className="progress-bar--percentage-text">{roundedPercent}%</div>
</div>
)}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as moment from 'moment';
import { remote } from 'electron';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
@@ -12,6 +12,7 @@ import {
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
export class About extends Contribution {
@@ -21,16 +22,20 @@ export class About extends Contribution {
@inject(ConfigService)
protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id,
label: `About ${this.applicationName}`,
label: nls.localize(
'arduino/about/label',
'About {0}',
this.applicationName
),
order: '0',
});
}
@@ -42,16 +47,24 @@ export class About extends Contribution {
status: cliStatus,
} = await this.configService.getVersion();
const buildDate = this.buildDate;
const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${
buildDate && showAll ? ` (${this.ago(buildDate)})` : ''
}
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
`;
const ok = 'OK';
const copy = 'Copy';
const detail = (showAll: boolean) =>
nls.localize(
'arduino/about/detail',
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
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',
new Date().getFullYear().toString()
)
);
const ok = nls.localize('vscode/issueMainService/ok', 'OK');
const copy = nls.localize('vscode/textInputActions/copy', 'Copy');
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
@@ -85,26 +98,86 @@ ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
const other = moment(isoTime);
let result = now.diff(other, 'minute');
if (result < 60) {
return result === 1 ? `${result} minute ago` : `${result} minute ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.minutes.singular.ago',
'{0} minute ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.minutes.plural.ago',
'{0} minutes ago',
result.toString()
);
}
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.hours.singular.ago',
'{0} hour ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.hours.plural.ago',
'{0} hours ago',
result.toString()
);
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.days.singular.ago',
'{0} day ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.days.plural.ago',
'{0} days ago',
result.toString()
);
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.weeks.singular.ago',
'{0} week ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.weeks.plural.ago',
'{0} weeks ago',
result.toString()
);
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1 ? `${result} month ago` : `${result} months ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.months.singular.ago',
'{0} month ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.months.plural.ago',
'{0} months ago',
result.toString()
);
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
return result === 1
? nls.localize(
'vscode/date/date.fromNow.years.singular.ago',
'{0} year ago',
result.toString()
)
: nls.localize(
'vscode/date/date.fromNow.years.plural.ago',
'{0} years ago',
result.toString()
);
}
}

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
SketchContribution,
@@ -9,33 +9,35 @@ import {
URI,
} from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id,
label: 'Add File...',
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
order: '2',
});
}
protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
title: nls.localize('arduino/contributions/addFile', 'Add File'),
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
@@ -50,9 +52,16 @@ export class AddFile extends SketchContribution {
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`,
title: nls.localize('arduino/contributions/replaceTitle', 'Replace'),
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/issueMainService/ok', 'OK'),
],
message: nls.localize(
'arduino/replaceMsg',
'Replace the existing version of {0}?',
filename
),
});
if (response === 0) {
// Cancel
@@ -60,9 +69,15 @@ export class AddFile extends SketchContribution {
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', {
timeout: 2000,
});
this.messageService.info(
nls.localize(
'arduino/contributions/fileAdded',
'One file added to the sketch.'
),
{
timeout: 2000,
}
);
}
}

View File

@@ -1,47 +1,47 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
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 { ResponseServiceImpl } from '../response-service-impl';
import { Installable, LibraryService } from '../../common/protocol';
import {
Installable,
LibraryService,
ResponseServiceArduino,
} from '../../common/protocol';
import {
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
label: 'Add .ZIP Library...',
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),
order: '1',
});
}
@@ -50,12 +50,15 @@ export class AddZipLibrary extends SketchContribution {
const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
title: "Select a zip file containing the library you'd like to add",
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
),
defaultPath,
properties: ['openFile'],
filters: [
{
name: 'Library',
name: nls.localize('arduino/library/zipLibrary', 'Library'),
extensions: ['zip'],
},
],
@@ -68,9 +71,12 @@ export class AddZipLibrary extends SketchContribution {
if (error instanceof AlreadyInstalledError) {
const result = await new ConfirmDialog({
msg: error.message,
title: 'Do you want to overwrite the existing library?',
ok: 'Yes',
cancel: 'No',
title: nls.localize(
'arduino/library/overwriteExistingLibrary',
'Do you want to overwrite the existing library?'
),
ok: nls.localize('vscode/extensionsUtils/yes', 'Yes'),
cancel: nls.localize('vscode/extensionsUtils/no', 'No'),
}).open();
if (result) {
await this.doInstall(zipUri, true);
@@ -84,14 +90,18 @@ export class AddZipLibrary extends SketchContribution {
try {
await Installable.doWithProgress({
messageService: this.messageService,
progressText: `Processing ${new URI(zipUri).path.base}`,
progressText:
nls.localize('arduino/common/processing', 'Processing') +
` ${new URI(zipUri).path.base}`,
responseService: this.responseService,
run: () => this.libraryService.installZip({ zipUri, overwrite }),
});
this.messageService.info(
`Successfully installed library from ${
nls.localize(
'arduino/library/successfullyInstalledZipLibrary',
'Successfully installed library from {0} archive',
new URI(zipUri).path.base
} archive`,
),
{ timeout: 3000 }
);
} catch (error) {
@@ -101,12 +111,19 @@ export class AddZipLibrary extends SketchContribution {
const name = match[1].trim();
if (name) {
throw new AlreadyInstalledError(
`A library folder named ${name} already exists. Do you want to overwrite it?`,
nls.localize(
'arduino/library/namedLibraryAlreadyExists',
'A library folder named {0} already exists. Do you want to overwrite it?',
name
),
name
);
} else {
throw new AlreadyInstalledError(
'A library already exists. Do you want to overwrite it?'
nls.localize(
'arduino/library/libraryAlreadyExists',
'A library already exists. Do you want to overwrite it?'
)
);
}
}

View File

@@ -1,5 +1,5 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
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';
@@ -9,19 +9,21 @@ import {
CommandRegistry,
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: 'Archive Sketch',
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
order: '1',
});
}
@@ -31,7 +33,7 @@ export class ArchiveSketch extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const archiveBasename = `${sketch.name}-${dateFormat(
@@ -42,7 +44,10 @@ export class ArchiveSketch extends SketchContribution {
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
});
if (!filePath || canceled) {
@@ -53,9 +58,16 @@ export class ArchiveSketch extends SketchContribution {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, {
timeout: 2000,
});
this.messageService.info(
nls.localize(
'arduino/sketch/createdArchive',
"Created archive '{0}'.",
archiveBasename
),
{
timeout: 2000,
}
);
}
}

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
DisposableCollection,
@@ -23,6 +23,7 @@ import {
Port,
} from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardSelection extends SketchContribution {
@@ -46,26 +47,36 @@ export class BoardSelection extends SketchContribution {
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) {
this.messageService.info(
'Please select a board to obtain board info.'
nls.localize(
'arduino/board/selectBoardForInfo',
'Please select a board to obtain board info.'
)
);
return;
}
if (!selectedBoard.fqbn) {
this.messageService.info(
`The platform for the selected '${selectedBoard.name}' board is not installed.`
nls.localize(
'arduino/board/platformMissing',
"The platform for the selected '{0}' board is not installed.",
selectedBoard.name
)
);
return;
}
if (!selectedPort) {
this.messageService.info(
'Please select a port to obtain board info.'
nls.localize(
'arduino/board/selectPortForInfo',
'Please select a port to obtain board info.'
)
);
return;
}
@@ -78,27 +89,31 @@ export class BoardSelection extends SketchContribution {
VID: ${VID}
PID: ${PID}`;
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: 'Board Info',
title: 'Board Info',
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
detail,
buttons: ['OK'],
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
});
}
},
});
}
onStart(): void {
override onStart(): void {
this.notificationCenter.onPlatformInstalled(() => this.updateMenus());
this.notificationCenter.onPlatformUninstalled(() => this.updateMenus());
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onAvailablePortsChanged(() =>
this.updateMenus()
);
}
override async onReady(): Promise<void> {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {
@@ -127,7 +142,11 @@ PID: ${PID}`;
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
boardsSubmenuPath,
`Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`,
nls.localize(
'arduino/board/board',
'Board{0}',
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
),
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
@@ -144,7 +163,11 @@ PID: ${PID}`;
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
nls.localize(
'arduino/board/port',
'Port{0}',
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
),
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
@@ -155,7 +178,7 @@ PID: ${PID}`;
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: 'Get Board Info',
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
order: '103',
};
this.menuModelRegistry.registerMenuAction(
@@ -173,17 +196,25 @@ PID: ${PID}`;
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...',
label: `${BoardsListWidget.WIDGET_LABEL}...`,
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel =
packageName +
`${manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
}`;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName);
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, {
order: packageName.toLowerCase(),
});
const id = `arduino-select-board--${fqbn}`;
const command = { id };
@@ -219,19 +250,25 @@ PID: ${PID}`;
}
// Installed ports
const registerPorts = (ports: AvailablePorts) => {
const addresses = Object.keys(ports);
if (!addresses.length) {
const registerPorts = (
protocol: string,
protocolOrder: number,
ports: AvailablePorts
) => {
const portIDs = Object.keys(ports);
if (!portIDs.length) {
return;
}
// Register placeholder for protocol
const [port] = ports[addresses[0]];
const protocol = port.protocol;
const menuPath = [...portsSubmenuPath, protocol];
const menuPath = [
...portsSubmenuPath,
`${protocolOrder.toString()}_${protocol}`,
];
const placeholder = new PlaceholderMenuNode(
menuPath,
`${firstToUpperCase(port.protocol)} ports`
`${firstToUpperCase(protocol)} ports`,
{ order: protocolOrder.toString() }
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
@@ -240,63 +277,75 @@ PID: ${PID}`;
)
);
for (const address of addresses) {
if (!!ports[address]) {
const [port, boards] = ports[address];
if (!boards.length) {
boards.push({
name: '',
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${
fqbn ? `--${fqbn}` : ''
}`;
const command = { id };
const handler = {
execute: () => {
if (
!Port.equals(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port,
};
}
},
isToggled: () =>
Port.equals(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
),
};
const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = {
commandId: id,
label,
order: `1${label}`, // `1` comes after the placeholder which has order `0`
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
// 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;
});
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
const [port, boards] = ports[portID];
let label = `${port.address}`;
if (boards.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
}
const id = `arduino-select-port--${portID}`;
const command = { id };
const handler = {
execute: () => {
if (
!Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port,
};
}
},
isToggled: () =>
Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
),
};
const menuAction = {
commandId: id,
label,
order: `${protocolOrder + i + 1}`,
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
};
const { serial, network, unknown } =
AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
const grouped = AvailablePorts.byProtocol(availablePorts);
let protocolOrder = 100;
// We first show serial and network ports, then all the rest
['serial', 'network'].forEach((protocol) => {
const ports = grouped.get(protocol);
if (ports) {
registerPorts(protocol, protocolOrder, ports);
grouped.delete(protocol);
protocolOrder = protocolOrder + 100;
}
});
grouped.forEach((ports, protocol) => {
registerPorts(protocol, protocolOrder, ports);
protocolOrder = protocolOrder + 100;
});
this.mainMenuManager.update();
}

View File

@@ -1,9 +1,8 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { inject, injectable } from '@theia/core/shared/inversify';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
@@ -11,14 +10,13 @@ import {
CommandRegistry,
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -27,30 +25,29 @@ export class BurnBootloader extends SketchContribution {
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
protected override readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: 'Burn Bootloader',
label: nls.localize(
'arduino/bootloader/burnBootloader',
'Burn Bootloader'
),
order: 'z99',
});
}
async burnBootloader(): Promise<void> {
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort?.address;
const port = boardsConfig.selectedPort;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
@@ -60,23 +57,37 @@ export class BurnBootloader extends SketchContribution {
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
board,
programmer,
port,
verify,
verbose,
});
this.messageService.info('Done burning bootloader.', {
timeout: 3000,
});
this.messageService.info(
nls.localize(
'arduino/bootloader/doneBurningBootloader',
'Done burning bootloader.'
),
{
timeout: 3000,
}
);
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
}
}
}

View File

@@ -1,12 +1,10 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { remote } from 'electron';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
Command,
@@ -15,6 +13,7 @@ import {
KeybindingRegistry,
URI,
} from './contribution';
import { nls } from '@theia/core/lib/common';
/**
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
@@ -22,83 +21,29 @@ import {
@injectable()
export class Close extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
protected shell: ApplicationShell;
onStart(app: FrontendApplication): void {
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => {
// Close current editor if closeable.
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) {
currentEditor.close();
return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message:
'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost.",
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
execute: () => remote.getCurrentWindow().close()
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: 'Close',
label: nls.localize('vscode/editor.contribution/close', 'Close'),
order: '5',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',

View File

@@ -1,4 +1,9 @@
import { inject, injectable, interfaces } from 'inversify';
import {
inject,
injectable,
interfaces,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Saveable } from '@theia/core/lib/browser/saveable';
@@ -9,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
MenuModelRegistry,
MenuContribution,
@@ -33,8 +38,11 @@ import {
CommandService,
} from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SettingsService } from '../dialogs/settings/settings';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
@@ -42,6 +50,7 @@ import {
Sketch,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
export {
Command,
@@ -84,15 +93,31 @@ export abstract class Contribution
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
onStart(app: FrontendApplication): MaybePromise<void> {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerCommands(registry: CommandRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerMenus(registry: MenuModelRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerKeybindings(registry: KeybindingRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerToolbarItems(registry: TabBarToolbarRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onReady(): MaybePromise<void> {}
}
@injectable()
@@ -127,7 +152,7 @@ export abstract class SketchContribution extends Contribution {
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
if (CurrentSketch.isValid(sketch)) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
@@ -140,7 +165,7 @@ export abstract class SketchContribution extends Contribution {
}
export namespace Contribution {
export function configure<T>(
export function configure(
bind: interfaces.Bind,
serviceIdentifier: typeof Contribution
): void {

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -12,6 +12,8 @@ import {
SketchContribution,
TabBarToolbarRegistry,
} from './contribution';
import { MaybePromise, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class Debug extends SketchContribution {
@@ -33,7 +35,10 @@ export class Debug extends SketchContribution {
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
protected _disabledMessages?: string = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
); // Initial pessimism.
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
protected onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
@@ -51,56 +56,42 @@ export class Debug extends SketchContribution {
command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${
this.disabledMessage
? `Debug - ${this.disabledMessage}`
: 'Start Debugging'
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
)
: Debug.Commands.START_DEBUGGING.label
}`,
priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
override onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
this.disabledMessage
? `Debug - ${this.disabledMessage}`
: 'Start Debugging'
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
)
: Debug.Commands.START_DEBUGGING.label
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
this.notificationCenter.onPlatformInstalled(() => this.refreshState());
this.notificationCenter.onPlatformUninstalled(() => this.refreshState());
}
registerCommands(registry: CommandRegistry): void {
override onReady(): MaybePromise<void> {
this.refreshState();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
execute: () => this.startDebug(),
isVisible: (widget) =>
@@ -109,10 +100,51 @@ export class Debug extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem(this.debugToolbarItem);
}
private async refreshState(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
}
protected async startDebug(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
@@ -129,7 +161,7 @@ export class Debug extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
@@ -155,10 +187,13 @@ export class Debug extends SketchContribution {
export namespace Debug {
export namespace Commands {
export const START_DEBUGGING: Command = {
id: 'arduino-start-debug',
label: 'Start Debugging',
category: 'Arduino',
};
export const START_DEBUGGING = Command.toLocalizedCommand(
{
id: 'arduino-start-debug',
label: 'Start Debugging',
category: 'Arduino',
},
'vscode/debug.contribution/startDebuggingHelp'
);
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
@@ -11,6 +11,9 @@ import {
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@@ -25,7 +28,7 @@ export class EditContributions extends Contribution {
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
execute: () => this.run('editor.action.gotoLine'),
});
@@ -42,10 +45,10 @@ export class EditContributions extends Contribution {
execute: () => this.run('actions.find'),
});
registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
execute: () => this.run('actions.findWithSelection'),
execute: () => this.run('editor.action.nextMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
execute: () => this.run('editor.action.nextMatchFindAction'),
execute: () => this.run('editor.action.previousMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
@@ -79,16 +82,6 @@ export class EditContributions extends Contribution {
{ execute: () => this.run('editor.action.formatDocument') }
);
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`[code]
${value}
[/code]`);
}
},
});
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
@@ -100,7 +93,7 @@ ${value}
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0',
@@ -111,95 +104,109 @@ ${value}
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
label: nls.localize(
'arduino/editor/copyForForum',
'Copy for Forum (Markdown)'
),
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
commandId: CommonCommands.PASTE.id,
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
commandId: CommonCommands.SELECT_ALL.id,
order: '4',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6',
label: nls.localize(
'vscode/standaloneStrings/gotoLineActionLabel',
'Go to Line...'
),
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
label: nls.localize(
'arduino/editor/commentUncomment',
'Comment/Uncomment'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
label: nls.localize('arduino/editor/increaseIndent', 'Increase Indent'),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
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: 'Decrease Font Size',
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
label: nls.localize('vscode/findController/startFindAction', 'Find'),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
label: nls.localize(
'vscode/findController/findNextMatchAction',
'Find Next'
),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
label: nls.localize(
'vscode/findController/findPreviousMatchAction',
'Find Previous'
),
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
label: nls.localize(
'vscode/findController/startFindWithSelectionAction',
'Use Selection for Find'
), // XXX: The Java IDE uses `Use Selection For Find`.
order: '3',
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'), // XXX: The Java IDE uses `Use Selection For Find`.
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
@@ -245,10 +252,10 @@ ${value}
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
protected async current(): Promise<ICodeEditor | StandaloneCodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
this.codeEditorService.getActiveCodeEditor() || undefined
);
}
@@ -280,9 +287,6 @@ export namespace EditContributions {
export const COPY_FOR_FORUM: Command = {
id: 'arduino-copy-for-forum',
};
export const COPY_FOR_GITHUB: Command = {
id: 'arduino-copy-for-github',
};
export const GO_TO_LINE: Command = {
id: 'arduino-go-to-line',
};

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import {
MenuPath,
@@ -21,7 +21,8 @@ import {
MenuModelRegistry,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, Sketch, SketchContainer } from '../../common/protocol';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
export abstract class Examples extends SketchContribution {
@@ -42,8 +43,8 @@ export abstract class Examples extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
@postConstruct()
init(): void {
protected override init(): void {
super.init();
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
@@ -53,7 +54,7 @@ export abstract class Examples extends SketchContribution {
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
@@ -69,15 +70,19 @@ export abstract class Examples extends SketchContribution {
}
// Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', {
order: '4',
});
registry.registerSubmenu(
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
nls.localize('arduino/examples/menu', 'Examples'),
{
order: '4',
}
);
}
registerRecursively(
sketchContainerOrPlaceholder:
| SketchContainer
| (Sketch | SketchContainer)[]
| (SketchRef | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
@@ -95,7 +100,7 @@ export abstract class Examples extends SketchContribution {
)
);
} else {
const sketches: Sketch[] = [];
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
@@ -156,7 +161,7 @@ export abstract class Examples extends SketchContribution {
@injectable()
export class BuiltInExamples extends Examples {
onStart(): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
@@ -166,11 +171,19 @@ export class BuiltInExamples extends Examples {
sketchContainers = await this.examplesService.builtIns();
} catch (e) {
console.error('Could not initialize built-in examples.', e);
this.messageService.error('Could not initialize built-in examples.');
this.messageService.error(
nls.localize(
'arduino/examples/couldNotInitializeExamples',
'Could not initialize built-in examples.'
)
);
return;
}
this.toDispose.dispose();
for (const container of ['Built-in examples', ...sketchContainers]) {
for (const container of [
nls.localize('arduino/examples/builtInExamples', 'Built-in examples'),
...sketchContainers,
]) {
this.registerRecursively(
container,
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
@@ -188,13 +201,16 @@ export class LibraryExamples extends Examples {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void {
this.register(); // no `await`
override onStart(): void {
this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register());
}
protected handleBoardChanged(board: Board | undefined): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
protected override handleBoardChanged(board: Board | undefined): void {
this.register(board);
}
@@ -211,13 +227,22 @@ export class LibraryExamples extends Examples {
fqbn,
});
if (user.length) {
(user as any).unshift('Examples from Custom Libraries');
(user as any).unshift(
nls.localize(
'arduino/examples/customLibrary',
'Examples from Custom Libraries'
)
);
}
if (name && fqbn && current.length) {
(current as any).unshift(`Examples for ${name}`);
(current as any).unshift(
nls.localize('arduino/examples/for', 'Examples for {0}', name)
);
}
if (any.length) {
(any as any).unshift('Examples for any board');
(any as any).unshift(
nls.localize('arduino/examples/forAny', 'Examples for any board')
);
}
for (const container of user) {
this.registerRecursively(

View File

@@ -0,0 +1,94 @@
import { MaybePromise } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { Formatter } from '../../common/protocol/formatter';
import { Contribution, URI } from './contribution';
@injectable()
export class Format
extends Contribution
implements
monaco.languages.DocumentRangeFormattingEditProvider,
monaco.languages.DocumentFormattingEditProvider
{
@inject(Formatter)
private readonly formatter: Formatter;
override onStart(): MaybePromise<void> {
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
monaco.languages.registerDocumentRangeFormattingEditProvider(
selector,
this
);
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
}
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const text = await this.format(model, range, options);
return [{ range, text }];
}
async provideDocumentFormattingEdits(
model: monaco.editor.ITextModel,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = this.fullRange(model);
const text = await this.format(model, range, options);
return [{ range, text }];
}
private fullRange(model: monaco.editor.ITextModel): monaco.Range {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
const end = new monaco.Position(lastLine, lastLineMaxColumn);
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
}
/**
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
* folder locations where the `.clang-format` file could be.
*/
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
const editorUri = new URI(model.uri.toString());
return this.workspaceService
.tryGetRoots()
.map(({ resource }) => resource)
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
.map((uri) => uri.toString());
}
private format(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions
): Promise<string> {
console.info(
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
range.toJSON()
)}]`
);
const content = model.getValueInRange(range);
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
return this.formatter.format({
content,
formatterConfigFolderUris,
options,
});
}
private selectorOf(
...languageId: string[]
): monaco.languages.LanguageSelector {
return languageId.map((language) => ({
language,
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}
}

View File

@@ -1,10 +1,10 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandHandler } from '@theia/core/lib/common/command';
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
import { ArduinoMenus } from '../menu/arduino-menus';
import { QuickInputService } from '@theia/core/lib/browser/quick-input/quick-input-service';
import {
Contribution,
Command,
@@ -12,6 +12,10 @@ import {
CommandRegistry,
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from '../ide-updater/ide-updater-commands';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class Help extends Contribution {
@@ -24,7 +28,7 @@ export class Help extends Contribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
const open = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) =>
@@ -60,9 +64,9 @@ export class Help extends Contribution {
}
}
if (!searchFor) {
searchFor = await this.quickInputService.open({
prompt: 'Search on Arduino.cc',
placeHolder: 'Type a keyword',
searchFor = await this.quickInputService.input({
prompt: nls.localize('arduino/help/search', 'Search on Arduino.cc'),
placeHolder: nls.localize('arduino/help/keyword', 'Type a keyword'),
});
}
if (searchFor) {
@@ -82,9 +86,17 @@ export class Help extends Contribution {
Help.Commands.VISIT_ARDUINO,
createOpenHandler('https://www.arduino.cc/')
);
registry.registerCommand(
Help.Commands.PRIVACY_POLICY,
createOpenHandler('https://www.arduino.cc/en/privacy-policy')
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.unregisterMenuAction({
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.GETTING_STARTED.id,
order: '0',
@@ -114,9 +126,17 @@ export class Help extends Contribution {
commandId: Help.Commands.VISIT_ARDUINO.id,
order: '6',
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.PRIVACY_POLICY.id,
order: '7',
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: IDEUpdaterCommands.CHECK_FOR_UPDATES.id,
order: '8',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F',
@@ -128,37 +148,42 @@ export namespace Help {
export namespace Commands {
export const GETTING_STARTED: Command = {
id: 'arduino-getting-started',
label: 'Getting Started',
label: nls.localize('arduino/help/gettingStarted', 'Getting Started'),
category: 'Arduino',
};
export const ENVIRONMENT: Command = {
id: 'arduino-environment',
label: 'Environment',
label: nls.localize('arduino/help/environment', 'Environment'),
category: 'Arduino',
};
export const TROUBLESHOOTING: Command = {
id: 'arduino-troubleshooting',
label: 'Troubleshooting',
label: nls.localize('arduino/help/troubleshooting', 'Troubleshooting'),
category: 'Arduino',
};
export const REFERENCE: Command = {
id: 'arduino-reference',
label: 'Reference',
label: nls.localize('arduino/help/reference', 'Reference'),
category: 'Arduino',
};
export const FIND_IN_REFERENCE: Command = {
id: 'arduino-find-in-reference',
label: 'Find in Reference',
label: nls.localize('arduino/help/findInReference', 'Find in Reference'),
category: 'Arduino',
};
export const FAQ: Command = {
id: 'arduino-faq',
label: 'Frequently Asked Questions',
label: nls.localize('arduino/help/faq', 'Frequently Asked Questions'),
category: 'Arduino',
};
export const VISIT_ARDUINO: Command = {
id: 'arduino-visit-arduino',
label: 'Visit Arduino.cc',
label: nls.localize('arduino/help/visit', 'Visit Arduino.cc'),
category: 'Arduino',
};
export const PRIVACY_POLICY: Command = {
id: 'arduino-privacy-policy',
label: nls.localize('arduino/help/privacyPolicy', 'Privacy Policy'),
category: 'Arduino',
};
}

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser';
@@ -15,6 +15,9 @@ import { LibraryListWidget } from '../library/library-list-widget';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -28,7 +31,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@@ -42,8 +45,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
onStart(): void {
this.updateMenuActions();
override onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions()
);
@@ -53,23 +55,34 @@ export class IncludeLibrary extends SketchContribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.updateMenuActions();
}
override registerMenus(registry: MenuModelRegistry): void {
// `Include Library` submenu
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
registry.registerSubmenu(
includeLibMenuPath,
nls.localize('arduino/library/include', 'Include Library'),
{
order: '1',
}
);
// `Manage Libraries...` group.
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...',
label: nls.localize(
'arduino/library/manageLibraries',
'Manage Libraries...'
),
});
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
@@ -101,10 +114,17 @@ export class IncludeLibrary extends SketchContribution {
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
(rest as any).unshift(
nls.localize('arduino/library/arduinoLibraries', 'Arduino libraries')
);
}
if (user.length) {
(user as any).unshift('Contributed libraries');
(user as any).unshift(
nls.localize(
'arduino/library/contributedLibraries',
'Contributed libraries'
)
);
}
for (const library of user) {
@@ -153,7 +173,7 @@ export class IncludeLibrary extends SketchContribution {
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.

View File

@@ -1,4 +1,5 @@
import { injectable } from 'inversify';
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 {
@@ -13,7 +14,7 @@ import {
@injectable()
export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(),
});
@@ -24,26 +25,26 @@ export class NewSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: 'New',
label: nls.localize('arduino/sketch/new', 'New'),
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
tooltip: 'New',
tooltip: nls.localize('arduino/sketch/new', 'New'),
priority: 3,
});
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import {
Disposable,
@@ -14,6 +14,7 @@ import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@@ -34,25 +35,31 @@ export class OpenRecentSketch extends SketchContribution {
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void {
const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches);
this.mainMenuManager.update();
};
override onStart(): void {
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
refreshMenu(sketches)
this.refreshMenu(sketches)
);
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.sketchService
.recentlyOpenedSketches()
.then((sketches) => this.refreshMenu(sketches));
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
'Open Recent',
nls.localize('arduino/sketch/openRecent', 'Open Recent'),
{ order: '2' }
);
}
private refreshMenu(sketches: Sketch[]): void {
this.register(sketches);
this.mainMenuManager.update();
}
protected register(sketches: Sketch[]): void {
const order = 0;
for (const sketch of sketches) {

View File

@@ -1,5 +1,5 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
@@ -9,24 +9,25 @@ import {
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: 'Show Sketch Folder',
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K',
@@ -36,7 +37,7 @@ export class OpenSketchExternal extends SketchContribution {
protected async openExternal(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) {
const exists = this.fileService.exists(new URI(uri));
const exists = await this.fileService.exists(new URI(uri));
if (exists) {
const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) {

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
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 {
@@ -22,6 +22,7 @@ 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';
@injectable()
export class OpenSketch extends SketchContribution {
@@ -42,7 +43,7 @@ export class OpenSketch extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
@@ -70,7 +71,10 @@ export class OpenSketch extends SketchContribution {
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
{
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
label: nls.localize(
'vscode/workspaceActions/openFileFolder',
'Open...'
),
}
);
this.toDispose.push(
@@ -112,26 +116,26 @@ export class OpenSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
order: '1',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
tooltip: nls.localize('vscode/dialogMainService/open', 'Open'),
priority: 4,
});
}
@@ -155,7 +159,7 @@ export class OpenSketch extends SketchContribution {
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
@@ -178,10 +182,18 @@ export class OpenSketch extends SketchContribution {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: 'Moving',
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: ['Cancel', 'OK'],
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`,
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
@@ -190,8 +202,12 @@ export class OpenSketch extends SketchContribution {
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: 'Error',
message: `A folder named "${name}" already exists. Can't open sketch.`,
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;
}

View File

@@ -1,5 +1,5 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX } from '@theia/core/lib/common/os';
import {
Contribution,
@@ -9,10 +9,11 @@ import {
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
@injectable()
export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(),
@@ -20,18 +21,18 @@ export class QuitApp extends Contribution {
}
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
commandId: QuitApp.Commands.QUIT_APP.id,
label: 'Quit',
label: nls.localize('vscode/bulkEditService/quit', 'Quit'),
order: '0',
});
}
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) {
registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id,

View File

@@ -1,5 +1,5 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
@@ -10,24 +10,39 @@ import {
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveAsSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
@inject(WindowService)
protected readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: 'Save As...',
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
order: '7',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
@@ -45,7 +60,7 @@ export class SaveAsSketch extends SketchContribution {
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@@ -73,7 +88,10 @@ export class SaveAsSketch extends SketchContribution {
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
if (!filePath || canceled) {
@@ -86,6 +104,9 @@ export class SaveAsSketch extends SketchContribution {
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri) {
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
}
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
@@ -96,12 +117,48 @@ export class SaveAsSketch extends SketchContribution {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.windowService.setSafeToShutDown();
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
return !!workspaceUri;
}
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, object>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
const uriString = uri?.toString();
let relativePath: string;
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (mainFileUri === uriString) {
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketchUri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
}));
}
}
export namespace SaveAsSketch {

View File

@@ -1,7 +1,8 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
Command,
@@ -10,10 +11,12 @@ import {
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
@@ -25,31 +28,47 @@ export class SaveSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: 'Save',
label: nls.localize('vscode/fileCommands/save', 'Save'),
order: '6',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
tooltip: 'Save',
tooltip: nls.localize('vscode/fileCommands/save', 'Save'),
priority: 5,
});
}
async saveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (isTemp) {
return this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: true,
}
);
}
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -7,7 +7,9 @@ import {
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings';
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 {
@@ -16,7 +18,7 @@ export class Settings extends SketchContribution {
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
@@ -37,16 +39,20 @@ export class Settings extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id,
label: 'Preferences...',
label:
nls.localize(
'vscode/preferences.contribution/preferences',
'Preferences'
) + '...',
order: '0',
});
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
@@ -58,7 +64,11 @@ export namespace Settings {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-settings-open',
label: 'Open Preferences...',
label:
nls.localize(
'vscode/preferences.contribution/openSettings2',
'Open Preferences'
) + '...',
category: 'Arduino',
};
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
@@ -19,8 +19,12 @@ import {
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@injectable()
export class SketchControl extends SketchContribution {
@@ -34,7 +38,7 @@ export class SketchControl extends SketchContribution {
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@@ -45,7 +49,7 @@ export class SketchControl extends SketchContribution {
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
{
@@ -54,7 +58,7 @@ export class SketchControl extends SketchContribution {
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
@@ -69,31 +73,28 @@ export class SketchControl extends SketchContribution {
return;
}
const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri);
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch =
await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentsketch = await this.sketchService.getSketchFolder(
parentsketchUri || ''
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
(await this.allowRename(parentsketch.uri))
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: 'Rename',
label: nls.localize('vscode/fileActions/rename', 'Rename'),
order: '1',
}
);
@@ -107,7 +108,7 @@ export class SketchControl extends SketchContribution {
} else {
const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Rename'
nls.localize('vscode/fileActions/rename', 'Rename')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -121,16 +122,16 @@ export class SketchControl extends SketchContribution {
}
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
(await this.allowDelete(parentsketch.uri))
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: 'Delete',
label: nls.localize('vscode/fileActions/delete', 'Delete'),
order: '2',
}
);
@@ -144,7 +145,7 @@ export class SketchControl extends SketchContribution {
} else {
const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Delete'
nls.localize('vscode/fileActions/delete', 'Delete')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -199,12 +200,12 @@ export class SketchControl extends SketchContribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
label: nls.localize('vscode/menubar/mNewTab', 'New Tab'),
order: '0',
}
);
@@ -213,7 +214,7 @@ export class SketchControl extends SketchContribution {
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
label: nls.localize('vscode/menubar/mShowPreviousTab', 'Previous Tab'),
order: '0',
}
);
@@ -221,13 +222,13 @@ export class SketchControl extends SketchContribution {
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
label: nls.localize('vscode/menubar/mShowNextTab', 'Next Tab'),
order: '0',
}
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
@@ -242,27 +243,31 @@ export class SketchControl extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
protected isCloudSketch(uri: string): boolean {
try {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
if (cloudCacheLocation) {
return true;
}
return false;
} catch {
return false;
}
return false;
}
protected async allowRename(uri: string) {
protected allowRename(uri: string): boolean {
return !this.isCloudSketch(uri);
}
protected async allowDelete(uri: string) {
protected allowDelete(uri: string): boolean {
return !this.isCloudSketch(uri);
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandRegistry, MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -7,14 +7,15 @@ import { NotificationCenter } from '../notification-center';
import { Examples } from './examples';
import { SketchContainer } from '../../common/protocol';
import { OpenSketch } from './open-sketch';
import { nls } from '@theia/core/lib/common';
@injectable()
export class Sketchbook extends Examples {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
protected override readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
protected override readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@@ -22,11 +23,7 @@ export class Sketchbook extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
onStart(): void {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
@@ -35,10 +32,17 @@ export class Sketchbook extends Examples {
});
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
'Sketchbook',
nls.localize('arduino/sketch/sketchbook', 'Sketchbook'),
{ order: '3' }
);
}
@@ -52,7 +56,7 @@ export class Sketchbook extends Examples {
);
}
protected createHandler(uri: string): CommandHandler {
protected override createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);

View File

@@ -0,0 +1,140 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadCertificateDialog } from '../dialogs/certificate-uploader/certificate-uploader-dialog';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import {
arduinoCert,
certificateList,
} from '../dialogs/certificate-uploader/utils';
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
@injectable()
export class UploadCertificate extends Contribution {
@inject(UploadCertificateDialog)
protected readonly dialog: UploadCertificateDialog;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => {
const certs = this.arduinoPreferences.get('arduino.board.certificates');
this.preferenceService.set(
'arduino.board.certificates',
certificateList(certs)
.filter((c) => c !== certToRemove)
.join(','),
PreferenceScope.User
);
},
isEnabled: (certToRemove) => certToRemove !== arduinoCert,
});
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
execute: async ({ fqbn, address, urls }) => {
return this.arduinoFirmwareUploader.uploadCertificates(
`-b ${fqbn} -a ${address} ${urls
.map((url: string) => `-u ${url}`)
.join(' ')}`
);
},
isEnabled: () => true,
});
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
execute: async (args: any) => {
this.contextMenuRenderer.render({
menuPath: ArduinoMenus.ROOT_CERTIFICATES__CONTEXT,
anchor: {
x: args.x,
y: args.y,
},
args: [args.cert],
});
},
isEnabled: () => true,
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadCertificate.Commands.OPEN.id,
label: UploadCertificate.Commands.OPEN.label,
order: '1',
});
registry.registerMenuAction(ArduinoMenus.ROOT_CERTIFICATES__CONTEXT, {
commandId: UploadCertificate.Commands.REMOVE_CERT.id,
label: UploadCertificate.Commands.REMOVE_CERT.label,
order: '1',
});
}
}
export namespace UploadCertificate {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-certificate-open',
label: nls.localize(
'arduino/certificate/uploadRootCertificates',
'Upload SSL Root Certificates'
),
category: 'Arduino',
};
export const OPEN_CERT_CONTEXT: Command = {
id: 'arduino-certificate-open-context',
label: nls.localize('arduino/certificate/openContext', 'Open context'),
category: 'Arduino',
};
export const REMOVE_CERT: Command = {
id: 'arduino-certificate-remove',
label: nls.localize('arduino/certificate/remove', 'Remove'),
category: 'Arduino',
};
export const UPLOAD_CERT: Command = {
id: 'arduino-certificate-upload',
label: nls.localize('arduino/certificate/upload', 'Upload'),
category: 'Arduino',
};
}
}

View File

@@ -0,0 +1,53 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadFirmwareDialog } from '../dialogs/firmware-uploader/firmware-uploader-dialog';
import { nls } from '@theia/core/lib/common';
@injectable()
export class UploadFirmware extends Contribution {
@inject(UploadFirmwareDialog)
protected readonly dialog: UploadFirmwareDialog;
protected dialogOpened = false;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadFirmware.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadFirmware.Commands.OPEN.id,
label: UploadFirmware.Commands.OPEN.label,
order: '0',
});
}
}
export namespace UploadFirmware {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-firmware-open',
label: nls.localize(
'arduino/firmware/updater',
'WiFi101 / WiFiNINA Firmware Updater'
),
category: 'Arduino',
};
}
}

View File

@@ -1,10 +1,9 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
@@ -14,14 +13,17 @@ import {
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -29,16 +31,89 @@ export class UploadSketch extends SketchContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(UserFieldsDialog)
protected readonly userFieldsDialog: UserFieldsDialog;
protected cachedUserFields: Map<string, BoardUserField[]> = new Map();
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected uploadInProgress = false;
protected boardRequiresUserFields = false;
registerCommands(registry: CommandRegistry): void {
protected readonly menuActionsDisposables = new DisposableCollection();
protected override init(): void {
super.init();
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceClientImpl;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) {
return '';
}
return fqbn + '|' + address;
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: () => this.uploadSketch(),
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceClientImpl.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
}
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress,
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
cached ??
(await this.boardsServiceClientImpl.selectedBoardUserFields())
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
});
registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
{
@@ -56,20 +131,50 @@ export class UploadSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '2',
});
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',
})
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
@@ -80,11 +185,11 @@ export class UploadSketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
tooltip: 'Upload',
tooltip: nls.localize('arduino/sketch/upload', 'Upload'),
priority: 1,
onDidChange: this.onDidChange,
});
@@ -101,18 +206,10 @@ export class UploadSketch extends SketchContribution {
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
let shouldAutoConnect = false;
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
if (this.monitorConnection.autoConnect) {
shouldAutoConnect = true;
}
this.monitorConnection.autoConnect = false;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
@@ -126,33 +223,51 @@ export class UploadSketch extends SketchContribution {
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort?.address;
const port = selectedPort;
const userFields =
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
if (userFields.length === 0 && this.boardRequiresUserFields) {
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
return;
}
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
board,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
userFields,
};
} else {
options = {
sketchUri,
fqbn,
board,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
userFields,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
@@ -161,32 +276,21 @@ export class UploadSketch extends SketchContribution {
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 3000 });
this.messageService.info(
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
{ timeout: 3000 }
);
} catch (e) {
this.messageService.error(e.toString());
let errorMessage = '';
if (typeof e === 'string') {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
if (monitorConfig) {
const { board, port } = monitorConfig;
try {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(board, { port }),
10_000
);
if (shouldAutoConnect) {
// Enabling auto-connect will trigger a connect.
this.monitorConnection.autoConnect = true;
} else {
await this.monitorConnection.connect(monitorConfig);
}
} catch (waitError) {
this.messageService.error(
`Could not reconnect to serial monitor. ${waitError.toString()}`
);
}
}
}
}
}
@@ -196,6 +300,14 @@ export namespace UploadSketch {
export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch',
};
export const UPLOAD_WITH_CONFIGURATION: Command = {
id: 'arduino-upload-with-configuration-sketch',
label: nls.localize(
'arduino/sketch/configureAndUpload',
'Configure And Upload'
),
category: 'Arduino',
};
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
id: 'arduino-upload-sketch-using-programmer',
};

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -13,6 +13,8 @@ import {
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class VerifySketch extends SketchContribution {
@@ -30,7 +32,7 @@ export class VerifySketch extends SketchContribution {
protected verifyInProgress = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(),
isEnabled: () => !this.verifyInProgress,
@@ -49,20 +51,23 @@ export class VerifySketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
label: 'Export compiled Binary',
order: '3',
label: nls.localize(
'arduino/sketch/exportBinary',
'Export Compiled Binary'
),
order: '4',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
@@ -73,11 +78,11 @@ export class VerifySketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
tooltip: 'Verify',
tooltip: nls.localize('arduino/sketch/verify', 'Verify'),
priority: 0,
onDidChange: this.onDidChange,
});
@@ -95,7 +100,7 @@ export class VerifySketch extends SketchContribution {
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
@@ -106,21 +111,35 @@ export class VerifySketch extends SketchContribution {
),
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
board,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
});
this.messageService.info('Done compiling.', { timeout: 3000 });
this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 }
);
} catch (e) {
this.messageService.error(e.toString());
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
@@ -15,6 +15,47 @@ export namespace ResponseResultProvider {
export const JSON: ResponseResultProvider = (response) => response.json();
}
export function Utf8ArrayToStr(array: Uint8Array): string {
let out, i, c;
let char2, char3;
out = '';
const len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12:
case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
);
break;
}
}
return out;
}
type ResourceType = 'f' | 'd';
@injectable()
@@ -59,14 +100,29 @@ export class CreateApi {
return result;
}
async sketches(): Promise<Create.Sketch[]> {
async sketches(limit = 50): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
url.searchParams.set('limit', limit.toString());
const headers = await this.headers();
const result = await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
const result: { sketches: Create.Sketch[] } = { sketches: [] };
let partialSketches: Create.Sketch[] = [];
let currentOffset = 0;
do {
url.searchParams.set('offset', currentOffset.toString());
partialSketches = (
await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
})
).sketches;
if (partialSketches.length !== 0) {
result.sketches = result.sketches.concat(partialSketches);
}
currentOffset = currentOffset + limit;
} while (partialSketches.length !== 0);
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;
}
@@ -275,9 +331,7 @@ export class CreateApi {
// parse the secret file
const secrets = (
typeof content === 'string'
? content
: new TextDecoder().decode(content)
typeof content === 'string' ? content : Utf8ArrayToStr(content)
)
.split(/\r?\n/)
.reduce((prev, curr) => {
@@ -341,7 +395,7 @@ export class CreateApi {
const headers = await this.headers();
let data: string =
typeof content === 'string' ? content : new TextDecoder().decode(content);
typeof content === 'string' ? content : Utf8ArrayToStr(content);
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
const payload = { data: btoa(data) };

View File

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

View File

@@ -97,6 +97,7 @@
"editorWhitespace.foreground": "#bfbfbf",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00",
"editorWidget.background": "#F7F9F9",
"focusBorder": "#7fcbcd99",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
import { nls } from '@theia/core/lib/common';
import * as React from '@theia/core/shared/react';
export const CertificateAddComponent = ({
addCertificate,
}: {
addCertificate: (cert: string) => void;
}): React.ReactElement => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[]
);
return (
<form
className="certificate-add"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
addCertificate(value);
setValue('');
}}
>
<label>
<div>
{nls.localize(
'arduino/certificate/addURL',
'Add URL to fetch SSL certificate'
)}
</div>
<input
className="theia-input"
placeholder={nls.localize(
'arduino/certificate/enterURL',
'Enter URL'
)}
type="text"
name="add"
onChange={handleChange}
value={value}
/>
</label>
</form>
);
};

View File

@@ -0,0 +1,51 @@
import * as React from '@theia/core/shared/react';
export const CertificateListComponent = ({
certificates,
selectedCerts,
setSelectedCerts,
openContextMenu,
}: {
certificates: string[];
selectedCerts: string[];
setSelectedCerts: React.Dispatch<React.SetStateAction<string[]>>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const handleOnChange = (event: any) => {
const target = event.target;
const newSelectedCerts = selectedCerts.filter(
(cert) => cert !== target.name
);
if (target.checked) {
newSelectedCerts.push(target.name);
}
setSelectedCerts(newSelectedCerts);
};
const handleContextMenu = (event: React.MouseEvent, cert: string) => {
openContextMenu(event.clientX, event.clientY, cert);
};
return (
<div className="certificate-list">
{certificates.map((certificate, i) => (
<label
key={i}
className="certificate-row"
onContextMenu={(e) => handleContextMenu(e, certificate)}
>
<span className="fl1">{certificate}</span>
<input
type="checkbox"
name={certificate}
checked={selectedCerts.includes(certificate)}
onChange={handleOnChange}
/>
</label>
))}
</div>
);
};

View File

@@ -0,0 +1,178 @@
import * as React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
import { SelectBoardComponent } from './select-board-components';
import { CertificateAddComponent } from './certificate-add-new';
import { nls } from '@theia/core/lib/common';
export const CertificateUploaderComponent = ({
availableBoards,
certificates,
addCertificate,
updatableFqbns,
uploadCertificates,
openContextMenu,
}: {
availableBoards: AvailableBoard[];
certificates: string[];
addCertificate: (cert: string) => void;
updatableFqbns: string[];
uploadCertificates: (
fqbn: string,
address: string,
urls: string[]
) => Promise<any>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [showAdd, setShowAdd] = React.useState(false);
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const installCertificates = async () => {
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
return;
}
setInstallFeedback('installing');
try {
await uploadCertificates(
selectedBoard.fqbn,
selectedBoard.port.address,
selectedCerts
);
setInstallFeedback('ok');
} catch {
setInstallFeedback('fail');
}
};
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<strong className="fl1">
{nls.localize(
'arduino/certificate/selectCertificateToUpload',
'1. Select certificate to upload'
)}
</strong>
<Tippy
content={
<CertificateAddComponent
addCertificate={(cert) => {
addCertificate(cert);
setShowAdd(false);
}}
/>
}
placement="bottom-end"
onClickOutside={() => setShowAdd(false)}
visible={showAdd}
interactive={true}
>
<button
type="button"
className="theia-button primary add-cert-btn"
onClick={() => {
showAdd ? setShowAdd(false) : setShowAdd(true);
}}
>
{nls.localize('arduino/certificate/addNew', 'Add New')}{' '}
<span className="fa fa-caret-down caret"></span>
</button>
</Tippy>
</div>
<div className="dialogRow">
<CertificateListComponent
certificates={certificates}
selectedCerts={selectedCerts}
setSelectedCerts={setSelectedCerts}
openContextMenu={openContextMenu}
/>
</div>
</div>
<div className="dialogSection">
<div className="dialogRow">
<strong>
{nls.localize(
'arduino/certificate/selectDestinationBoardToUpload',
'2. Select destination board and upload certificate'
)}
</strong>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
</div>
<div className="dialogRow">
<div className="upload-status">
{installFeedback === 'installing' && (
<div className="success">
<div className="spinner" />
{nls.localize(
'arduino/certificate/uploadingCertificates',
'Uploading certificates.'
)}
</div>
)}
{installFeedback === 'ok' && (
<div className="success">
<i className="fa fa-info status-icon" />
{nls.localize(
'arduino/certificate/certificatesUploaded',
'Certificates uploaded.'
)}
</div>
)}
{installFeedback === 'fail' && (
<div className="warn">
<i className="fa fa-exclamation status-icon" />
{nls.localize(
'arduino/certificate/uploadFailed',
'Upload failed. Please try again.'
)}
</div>
)}
</div>
<button
type="button"
className="theia-button primary install-cert-btn"
onClick={installCertificates}
disabled={selectedCerts.length === 0 || !selectedBoard}
>
{nls.localize('arduino/certificate/upload', 'Upload')}
</button>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,197 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoPreferences.ready.then(() => {
this.certificates = certificateList(
this.arduinoPreferences.get('arduino.board.certificates')
);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.board.certificates' &&
event.newValue !== event.oldValue
) {
this.certificates = certificateList(event.newValue);
this.update();
}
});
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
private addCertificate(certificate: string) {
const certString = sanifyCertString(certificate);
if (certString.length > 0) {
this.certificates.push(sanifyCertString(certificate));
}
this.preferenceService.set(
'arduino.board.certificates',
this.certificates.join(','),
PreferenceScope.User
);
}
protected openContextMenu(x: number, y: number, cert: string): void {
this.commandRegistry.executeCommand(
'arduino-certificate-open-context',
Object.assign({}, { x, y, cert })
);
}
protected uploadCertificates(
fqbn: string,
address: string,
urls: string[]
): Promise<any> {
this.busyCallback(true);
return this.commandRegistry
.executeCommand('arduino-certificate-upload', {
fqbn,
address,
urls,
})
.finally(() => this.busyCallback(false));
}
protected render(): React.ReactNode {
return (
<CertificateUploaderComponent
availableBoards={this.availableBoards}
certificates={this.certificates}
updatableFqbns={this.updatableFqbns}
addCertificate={this.addCertificate.bind(this)}
uploadCertificates={this.uploadCertificates.bind(this)}
openContextMenu={this.openContextMenu.bind(this)}
/>
);
}
}
@injectable()
export class UploadCertificateDialogProps extends DialogProps {}
@injectable()
export class UploadCertificateDialog extends AbstractDialog<void> {
@inject(UploadCertificateDialogWidget)
protected readonly widget: UploadCertificateDialogWidget;
private busy = false;
constructor(
@inject(UploadCertificateDialogProps)
protected override readonly props: UploadCertificateDialogProps
) {
super({
title: nls.localize(
'arduino/certificate/uploadRootCertificates',
'Upload SSL Root Certificates'
),
});
this.contentNode.classList.add('certificate-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
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);
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 handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
override close(): void {
if (this.busy) {
return;
}
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@@ -0,0 +1,108 @@
import { nls } from '@theia/core/lib/common';
import * as React from '@theia/core/shared/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
type BoardOption = { value: string; label: string };
export const SelectBoardComponent = ({
availableBoards,
updatableFqbns,
onBoardSelect,
selectedBoard,
busy,
}: {
availableBoards: AvailableBoard[];
updatableFqbns: string[];
onBoardSelect: (board: AvailableBoard | null) => void;
selectedBoard: AvailableBoard | null;
busy: boolean;
}): React.ReactElement => {
const [selectOptions, setSelectOptions] = React.useState<BoardOption[]>([]);
const [selectBoardPlaceholder, setSelectBoardPlaceholder] =
React.useState('');
const selectOption = React.useCallback(
(boardOpt: BoardOption) => {
onBoardSelect(
(boardOpt &&
availableBoards.find((board) => board.fqbn === boardOpt.value)) ||
null
);
},
[availableBoards, onBoardSelect]
);
React.useEffect(() => {
// if there is activity going on, skip updating the boards (avoid flickering)
if (busy) {
return;
}
let placeholderTxt = nls.localize(
'arduino/certificate/selectBoard',
'Select a board...'
);
let selBoard = -1;
const updatableBoards = availableBoards.filter(
(board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn)
);
const boardsList: BoardOption[] = updatableBoards.map((board, i) => {
if (board.selected) {
selBoard = i;
}
return {
label: nls.localize(
'arduino/certificate/boardAtPort',
'{0} at {1}',
board.name,
board.port?.address ?? ''
),
value: board.fqbn || '',
};
});
if (boardsList.length === 0) {
placeholderTxt = nls.localize(
'arduino/certificate/noSupportedBoardConnected',
'No supported board connected'
);
}
setSelectBoardPlaceholder(placeholderTxt);
setSelectOptions(boardsList);
if (selectedBoard) {
selBoard = boardsList
.map((boardOpt) => boardOpt.value)
.indexOf(selectedBoard.fqbn || '');
}
selectOption(boardsList[selBoard] || null);
}, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]);
return (
<ArduinoSelect
id="board-select"
menuPosition="fixed"
isDisabled={selectOptions.length === 0 || busy}
placeholder={selectBoardPlaceholder}
options={selectOptions}
value={
(selectedBoard && {
value: selectedBoard.fqbn,
label: nls.localize(
'arduino/certificate/boardAtPort',
'{0} at {1}',
selectedBoard.name,
selectedBoard.port?.address ?? ''
),
}) ||
null
}
tabSelectsValue={false}
onChange={selectOption}
/>
);
};

View File

@@ -0,0 +1,38 @@
export const arduinoCert = 'arduino.cc:443';
export function sanifyCertString(cert: string): string {
const regex = /^(?:.*:\/\/)*(\S+\.+[^:]*):*(\d*)*$/gm;
const m = regex.exec(cert);
if (!m) {
return '';
}
const domain = m[1] || '';
const port = m[2] || '443';
if (domain.length === 0 || port.length === 0) {
return '';
}
return `${domain}:${port}`;
}
export function certificateList(certificates: string): string[] {
let certs = certificates
.split(',')
.map((cert) => sanifyCertString(cert.trim()))
.filter((cert) => {
// remove empty certificates
if (!cert || cert.length === 0) {
return false;
}
return true;
});
// add arduino certificate at the top of the list
certs = certs.filter((cert) => cert !== arduinoCert);
certs.unshift(arduinoCert);
return certs;
}

View File

@@ -1,14 +1,12 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from 'electron';
import {
AbstractDialog,
ReactWidget,
DialogProps,
} from '@theia/core/lib/browser';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { CreateApi } from '../create/create-api';
import { nls } from '@theia/core/lib/common';
const RadioButton = (props: {
id: string;
@@ -42,10 +40,6 @@ export const ShareSketchComponent = ({
createApi: CreateApi;
domain?: string;
}): React.ReactElement => {
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
// treeNode.isPublic
// );
const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
@@ -66,12 +60,20 @@ export const ShareSketchComponent = ({
return (
<div id="widget-container arduino-sharesketch-dialog">
<p>Choose visibility of your Sketch:</p>
<p>
{nls.localize(
'arduino/cloud/chooseSketchVisibility',
'Choose visibility of your Sketch:'
)}
</p>
<RadioButton
changed={radioChangeHandler}
id="1"
isSelected={treeNode.isPublic === false}
label="Private. Only you can view the Sketch."
label={nls.localize(
'arduino/cloud/privateVisibility',
'Private. Only you can view the Sketch.'
)}
value="private"
isDisabled={loading}
/>
@@ -79,14 +81,17 @@ export const ShareSketchComponent = ({
changed={radioChangeHandler}
id="2"
isSelected={treeNode.isPublic === true}
label="Public. Anyone with the link can view the Sketch."
label={nls.localize(
'arduino/cloud/publicVisibility',
'Public. Anyone with the link can view the Sketch.'
)}
value="public"
isDisabled={loading}
/>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<p>{nls.localize('arduino/cloud/link', 'Link:')}</p>
<div className="sketch-link">
<input
type="text"
@@ -99,10 +104,10 @@ export const ShareSketchComponent = ({
value="copy"
className="theia-button secondary"
>
Copy
{nls.localize('vscode/textInputActions/copy', 'Copy')}
</button>
</div>
<p>Embed:</p>
<p>{nls.localize('arduino/cloud/embed', 'Embed:')}</p>
<div className="sketch-link-embed">
<textarea
readOnly
@@ -144,7 +149,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
protected override readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
@@ -154,7 +159,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -163,12 +168,12 @@ export class ShareSketchDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}

View File

@@ -1,14 +1,15 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
ConfirmDialogProps,
DialogError,
} from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common';
@injectable()
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps {
export class DoNotAskAgainDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>;
}
@@ -17,8 +18,8 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor(
@inject(DoNotAskAgainConfirmDialogProps)
protected readonly props: DoNotAskAgainConfirmDialogProps
@inject(DoNotAskAgainDialogProps)
protected override readonly props: DoNotAskAgainDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
@@ -31,14 +32,17 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
const doNotAskAgainLabel = document.createElement('label');
doNotAskAgainLabel.classList.add('flex-line');
doNotAskAgainNode.appendChild(doNotAskAgainLabel);
doNotAskAgainLabel.textContent = "Don't ask again";
doNotAskAgainLabel.textContent = nls.localize(
'arduino/dialog/dontAskAgain',
"Don't ask again"
);
this.doNotAskAgainCheckbox = document.createElement('input');
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
@@ -61,7 +65,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
}
}
protected setErrorMessage(error: DialogError): void {
protected override setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}

View File

@@ -0,0 +1,223 @@
import { nls } from '@theia/core/lib/common';
import * as React from '@theia/core/shared/react';
import { Port } from '../../../common/protocol';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SelectBoardComponent } from '../certificate-uploader/select-board-components';
type FirmwareOption = { value: string; label: string };
export const FirmwareUploaderComponent = ({
availableBoards,
firmwareUploader,
updatableFqbns,
flashFirmware,
isOpen,
}: {
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
const [firmwaresFetching, setFirmwaresFetching] = React.useState(false);
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const [availableFirmwares, setAvailableFirmwares] = React.useState<
FirmwareInfo[]
>([]);
React.useEffect(() => {
setAvailableFirmwares([]);
}, [isOpen]);
const [selectedFirmware, setSelectedFirmware] =
React.useState<FirmwareOption | null>(null);
const [firmwareOptions, setFirmwareOptions] = React.useState<
FirmwareOption[]
>([]);
const fetchFirmwares = React.useCallback(async () => {
setInstallFeedback(null);
setFirmwaresFetching(true);
if (!selectedBoard) {
return;
}
// fetch the firmwares for the selected board
const firmwaresForFqbn = await firmwareUploader.availableFirmwares(
selectedBoard.fqbn || ''
);
setAvailableFirmwares(firmwaresForFqbn);
const firmwaresOpts = firmwaresForFqbn.map((f) => ({
label: f.firmware_version,
value: f.firmware_version,
}));
setFirmwareOptions(firmwaresOpts);
if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]);
setFirmwaresFetching(false);
}, [firmwareUploader, selectedBoard]);
const installFirmware = React.useCallback(async () => {
setInstallFeedback('installing');
const firmwareToFlash = availableFirmwares.find(
(firmware) => firmware.firmware_version === selectedFirmware?.value
);
try {
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {
setInstallFeedback('fail');
}
}, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]);
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setAvailableFirmwares([]);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="board-select">
{nls.localize('arduino/firmware/selectBoard', 'Select Board')}
</label>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
<button
type="button"
className="theia-button secondary"
disabled={
selectedBoard === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={fetchFirmwares}
>
{nls.localize('arduino/firmware/checkUpdates', 'Check Updates')}
</button>
</div>
</div>
{availableFirmwares.length > 0 && (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="firmware-select" className="fl1">
{nls.localize(
'arduino/firmware/selectVersion',
'Select firmware version'
)}
</label>
<ArduinoSelect
id="firmware-select"
menuPosition="fixed"
isDisabled={
!selectedBoard ||
firmwaresFetching ||
installFeedback === 'installing'
}
options={firmwareOptions}
value={selectedFirmware}
tabSelectsValue={false}
onChange={(value) => {
if (value) {
setInstallFeedback(null);
setSelectedFirmware(value);
}
}}
/>
<button
type="button"
className="theia-button primary"
disabled={
selectedFirmware === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={installFirmware}
>
{nls.localize('arduino/firmware/install', 'Install')}
</button>
</div>
</div>
<div className="dialogSection">
{installFeedback === null && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
{nls.localize(
'arduino/firmware/overwriteSketch',
'Installation will overwrite the Sketch on the board.'
)}
</div>
)}
{installFeedback === 'installing' && (
<div className="dialogRow success">
<div className="spinner" />
{nls.localize(
'arduino/firmware/installingFirmware',
'Installing firmware.'
)}
</div>
)}
{installFeedback === 'ok' && (
<div className="dialogRow success">
<i className="fa fa-info status-icon" />
{nls.localize(
'arduino/firmware/successfullyInstalled',
'Firmware successfully installed.'
)}
</div>
)}
{installFeedback === 'fail' && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
{nls.localize(
'arduino/firmware/failedInstall',
'Installation failed. Please try again.'
)}
</div>
)}
</div>
</>
)}
</>
);
};

View File

@@ -0,0 +1,152 @@
import * as React from '@theia/core/shared/react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.appStatusService.reachedState('ready').then(async () => {
const fqbns = await this.arduinoFirmwareUploader.updatableBoards();
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
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.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
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);
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 handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
override close(): void {
if (this.busy) {
return;
}
this.widget.close();
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@@ -0,0 +1,210 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';
export type IDEUpdaterComponentProps = {
updateInfo: UpdateInfo;
windowService: WindowService;
downloadFinished?: boolean;
downloadStarted?: boolean;
progress?: ProgressInfo;
error?: Error;
onDownload: () => void;
onClose: () => void;
onSkipVersion: () => void;
onCloseAndInstall: () => void;
};
export const IDEUpdaterComponent = ({
updateInfo: { version, releaseNotes },
downloadStarted = false,
downloadFinished = false,
windowService,
progress,
error,
onDownload,
onClose,
onSkipVersion,
onCloseAndInstall,
}: IDEUpdaterComponentProps): React.ReactElement => {
const changelogDivRef = React.useRef() as React.MutableRefObject<
HTMLDivElement
>;
React.useEffect(() => {
if (!!releaseNotes) {
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
);
}
}, [releaseNotes]);
const closeButton = (
<button onClick={onClose} type="button" className="theia-button secondary">
{nls.localize('arduino/ide-updater/notNowButton', 'Not now')}
</button>
);
const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded">
<div>
{nls.localize(
'arduino/ide-updater/versionDownloaded',
'Arduino IDE {0} has been downloaded.',
version
)}
</div>
<div>
{nls.localize(
'arduino/ide-updater/closeToInstallNotice',
'Close the software and install the update on your machine.'
)}
</div>
<div className="buttons-container">
{closeButton}
<button
onClick={onCloseAndInstall}
type="button"
className="theia-button close-and-install"
>
{nls.localize(
'arduino/ide-updater/closeAndInstallButton',
'Close and Install'
)}
</button>
</div>
</div>
);
const DownloadStarted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloading">
<div>
{nls.localize(
'arduino/ide-updater/downloadingNotice',
'Downloading the latest version of the Arduino IDE.'
)}
</div>
<ProgressBar percent={progress?.percent} showPercentage />
</div>
);
const PreDownload: () => React.ReactElement = () => (
<div className="ide-updater-dialog--pre-download">
<div className="ide-updater-dialog--logo-container">
<div className="ide-updater-dialog--logo"></div>
</div>
<div className="ide-updater-dialog--new-version-text dialogSection">
<div className="dialogRow">
<div className="bold">
{nls.localize(
'arduino/ide-updater/updateAvailable',
'Update Available'
)}
</div>
</div>
<div className="dialogRow">
{nls.localize(
'arduino/ide-updater/newVersionAvailable',
'A new version of Arduino IDE ({0}) is available for download.',
version
)}
</div>
{releaseNotes && (
<div className="dialogRow">
<div className="changelog-container" ref={changelogDivRef} />
</div>
)}
<div className="buttons-container">
<button
onClick={onSkipVersion}
type="button"
className="theia-button secondary skip-version"
>
{nls.localize(
'arduino/ide-updater/skipVersionButton',
'Skip Version'
)}
</button>
<div className="push"></div>
{closeButton}
<button
onClick={onDownload}
type="button"
className="theia-button primary"
>
{nls.localize('arduino/ide-updater/downloadButton', 'Download')}
</button>
</div>
</div>
</div>
);
const onGoToDownloadClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
event.nativeEvent.preventDefault();
windowService.openNewWindow(target.href, { external: true });
onClose();
}
};
const GoToDownloadPage: () => React.ReactElement = () => (
<div className="ide-updater-dialog--go-to-download-page">
<div>
{nls.localize(
'arduino/ide-updater/goToDownloadPage',
"An update for the Arduino IDE is available, but we're not able to download and install it automatically. Please go to the download page and download the latest version from there."
)}
</div>
<div className="buttons-container">
{closeButton}
<a
className="theia-button primary"
href="https://www.arduino.cc/en/software#experimental-software"
onClick={onGoToDownloadClick}
>
{nls.localize(
'arduino/ide-updater/goToDownloadButton',
'Go To Download'
)}
</a>
</div>
</div>
);
return (
<div className="ide-updater-dialog--content">
{!!error ? (
<GoToDownloadPage />
) : downloadFinished ? (
<DownloadCompleted />
) : downloadStarted ? (
<DownloadStarted />
) : (
<PreDownload />
)}
</div>
);
};

View File

@@ -0,0 +1,173 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable } 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 { nls } from '@theia/core';
import { IDEUpdaterComponent } from './ide-updater-component';
import {
IDEUpdater,
IDEUpdaterClient,
ProgressInfo,
UpdateInfo,
} from '../../../common/protocol/ide-updater';
import { LocalStorageService } from '@theia/core/lib/browser';
import { SKIP_IDE_VERSION } from '../../arduino-frontend-contribution';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {
protected isOpen = new Object();
updateInfo: UpdateInfo;
progressInfo: ProgressInfo | undefined;
error: Error | undefined;
downloadFinished: boolean;
downloadStarted: boolean;
onClose: () => void;
@inject(IDEUpdater)
protected readonly updater: IDEUpdater;
@inject(IDEUpdaterClient)
protected readonly updaterClient: IDEUpdaterClient;
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(WindowService)
protected windowService: WindowService;
init(updateInfo: UpdateInfo, onClose: () => void): void {
this.updateInfo = updateInfo;
this.progressInfo = undefined;
this.error = undefined;
this.downloadStarted = false;
this.downloadFinished = false;
this.onClose = onClose;
this.updaterClient.onError((e) => {
this.error = e;
this.update();
});
this.updaterClient.onDownloadProgressChanged((e) => {
this.progressInfo = e;
this.update();
});
this.updaterClient.onDownloadFinished((e) => {
this.downloadFinished = true;
this.update();
});
}
async onSkipVersion(): Promise<void> {
this.localStorageService.setData<string>(
SKIP_IDE_VERSION,
this.updateInfo.version
);
this.close();
}
override close(): void {
super.close();
this.onClose();
}
onDispose(): void {
if (this.downloadStarted && !this.downloadFinished)
this.updater.stopDownload();
}
async onDownload(): Promise<void> {
this.progressInfo = undefined;
this.downloadStarted = true;
this.error = undefined;
this.updater.downloadUpdate();
this.update();
}
onCloseAndInstall(): void {
this.updater.quitAndInstall();
}
protected render(): React.ReactNode {
return !!this.updateInfo ? (
<form>
<IDEUpdaterComponent
updateInfo={this.updateInfo}
windowService={this.windowService}
downloadStarted={this.downloadStarted}
downloadFinished={this.downloadFinished}
progress={this.progressInfo}
error={this.error}
onClose={this.close.bind(this)}
onSkipVersion={this.onSkipVersion.bind(this)}
onDownload={this.onDownload.bind(this)}
onCloseAndInstall={this.onCloseAndInstall.bind(this)}
/>
</form>
) : null;
}
}
@injectable()
export class IDEUpdaterDialogProps extends DialogProps {}
@injectable()
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(IDEUpdaterDialogWidget)
protected readonly widget: IDEUpdaterDialogWidget;
constructor(
@inject(IDEUpdaterDialogProps)
protected override readonly props: IDEUpdaterDialogProps
) {
super({
title: nls.localize(
'arduino/ide-updater/ideUpdaterDialog',
'Software Update'
),
});
this.contentNode.classList.add('ide-updater-dialog');
this.acceptButton = undefined;
}
get value(): UpdateInfo {
return this.widget.updateInfo;
}
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();
}
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
this.widget.init(data, this.close.bind(this));
return super.open();
}
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
override close(): void {
this.widget.dispose();
super.close();
}
}

View File

@@ -0,0 +1,781 @@
import * as React from '@theia/core/shared/react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import { Disable } from 'react-disable';
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';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
AdditionalUrls,
CompilerWarningLiterals,
Network,
ProxySettings,
} from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { Settings, SettingsService } from './settings';
import { AdditionalUrlsDialog } from './settings-dialog';
import {
AsyncLocalizationProvider,
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
export class SettingsComponent extends React.Component<
SettingsComponent.Props,
SettingsComponent.State
> {
readonly toDispose = new DisposableCollection();
constructor(props: SettingsComponent.Props) {
super(props);
}
override componentDidUpdate(
_: SettingsComponent.Props,
prevState: SettingsComponent.State
): void {
if (
this.state &&
prevState &&
JSON.stringify(SettingsComponent.State.toSettings(this.state)) !==
JSON.stringify(SettingsComponent.State.toSettings(prevState))
) {
this.props.settingsService.update(
SettingsComponent.State.toSettings(this.state),
true
);
}
}
override componentDidMount(): void {
this.props.settingsService
.settings()
.then((settings) =>
this.setState(SettingsComponent.State.fromSettings(settings))
);
this.toDispose.pushAll([
this.props.settingsService.onDidChange((settings) =>
this.setState((prevState) => ({
...SettingsComponent.State.merge(prevState, settings),
}))
),
this.props.settingsService.onDidReset((settings) =>
this.setState(SettingsComponent.State.fromSettings(settings))
),
]);
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
if (!this.state) {
return <div />;
}
return (
<Tabs>
<TabList>
<Tab>{nls.localize('vscode/settingsTree/settings', 'Settings')}</Tab>
<Tab>{nls.localize('arduino/preferences/network', 'Network')}</Tab>
</TabList>
<TabPanel>{this.renderSettings()}</TabPanel>
<TabPanel>{this.renderNetwork()}</TabPanel>
</Tabs>
);
}
protected renderSettings(): React.ReactNode {
return (
<div className="content noselect">
{nls.localize(
'arduino/preferences/sketchbook.location',
'Sketchbook location'
) + ':'}
<div className="flex-line">
<input
className="theia-input stretch"
type="text"
value={this.state.sketchbookPath}
onChange={this.sketchpathDidChange}
/>
<button
className="theia-button shrink"
onClick={this.browseSketchbookDidClick}
>
{nls.localize('arduino/preferences/browse', 'Browse')}
</button>
</div>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.sketchbookShowAllFiles === true}
onChange={this.sketchbookShowAllFilesDidChange}
/>
{nls.localize(
'arduino/preferences/files.inside.sketches',
'Show files inside Sketches'
)}
</label>
<div className="flex-line">
<div className="column">
<div className="flex-line">
{nls.localize(
'arduino/preferences/editorFontSize',
'Editor font size'
) + ':'}
</div>
<div className="flex-line">
{nls.localize(
'arduino/preferences/interfaceScale',
'Interface scale'
) + ':'}
</div>
<div className="flex-line">
{nls.localize(
'vscode/themes.contribution/selectTheme.label',
'Theme'
) + ':'}
</div>
<div className="flex-line">
{nls.localize(
'vscode/editorStatus/status.editor.mode',
'Language'
) + ':'}
</div>
<div className="flex-line">
{nls.localize(
'arduino/preferences/showVerbose',
'Show verbose output during'
)}
</div>
<div className="flex-line">
{nls.localize(
'arduino/preferences/compilerWarnings',
'Compiler warnings'
)}
</div>
</div>
<div className="column">
<div className="flex-line">
<input
className="theia-input small"
type="number"
step={1}
pattern="[0-9]+"
onKeyDown={this.numbersOnlyKeyDown}
value={this.state.editorFontSize}
onChange={this.editorFontSizeDidChange}
/>
</div>
<div className="flex-line">
<label className="flex-line">
<input
type="checkbox"
checked={this.state.autoScaleInterface}
onChange={this.autoScaleInterfaceDidChange}
/>
{nls.localize('arduino/preferences/automatic', 'Automatic')}
</label>
<input
className="theia-input small with-margin"
type="number"
step={20}
pattern="[0-9]+"
onKeyDown={this.noopKeyDown}
value={100 + this.state.interfaceScale * 20}
onChange={this.interfaceScaleDidChange}
/>
%
</div>
<div className="flex-line">
<select
className="theia-select"
value={
ThemeService.get()
.getThemes()
.find(({ id }) => id === this.state.themeId)?.label ||
nls.localize('arduino/common/unknown', 'Unknown')
}
onChange={this.themeDidChange}
>
{ThemeService.get()
.getThemes()
.map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
</select>
</div>
<div className="flex-line">
<select
className="theia-select"
value={this.state.currentLanguage}
onChange={this.languageDidChange}
>
{this.state.languages.map((label) =>
this.toSelectOptions(label)
)}
</select>
<span style={{ marginLeft: '5px' }}>
(
{nls.localize(
'vscode/extensionsActions/reloadRequired',
'Reload required'
)}
)
</span>
</div>
<div className="flex-line">
<label className="flex-line">
<input
type="checkbox"
checked={this.state.verboseOnCompile}
onChange={this.verboseOnCompileDidChange}
/>
{nls.localize('arduino/preferences/compile', 'compile')}
</label>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.verboseOnUpload}
onChange={this.verboseOnUploadDidChange}
/>
{nls.localize('arduino/preferences/upload', 'upload')}
</label>
</div>
<div className="flex-line">
<select
className="theia-select"
value={this.state.compilerWarnings}
onChange={this.compilerWarningsDidChange}
>
{CompilerWarningLiterals.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</div>
</div>
</div>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.verifyAfterUpload}
onChange={this.verifyAfterUploadDidChange}
/>
{nls.localize(
'arduino/preferences/verifyAfterUpload',
'Verify code after upload'
)}
</label>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.autoSave !== 'off'}
onChange={this.autoSaveDidChange}
/>
{nls.localize(
'vscode/fileActions.contribution/miAutoSave',
'Auto save'
)}
</label>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.quickSuggestions.other === true}
onChange={this.quickSuggestionsOtherDidChange}
/>
{nls.localize(
'arduino/preferences/editorQuickSuggestions',
'Editor Quick Suggestions'
)}
</label>
<div className="flex-line">
{nls.localize(
'arduino/preferences/additionalManagerURLs',
'Additional boards manager URLs'
) + ':'}
<input
className="theia-input stretch with-margin"
type="text"
value={this.state.rawAdditionalUrlsValue}
onChange={this.rawAdditionalUrlsValueDidChange}
/>
<i
className="fa fa-window-restore theia-button shrink"
onClick={this.editAdditionalUrlDidClick}
/>
</div>
</div>
);
}
private toSelectOptions(language: string | LanguageInfo): JSX.Element {
const plain = typeof language === 'string';
const key = plain ? language : language.languageId;
const value = plain ? language : language.languageId;
const label = plain
? language === 'en'
? 'English'
: language
: language.localizedLanguageName ||
language.languageName ||
language.languageId;
return (
<option key={key} value={value}>
{label}
</option>
);
}
protected renderNetwork(): React.ReactNode {
return (
<div className="content noselect">
<form>
<label className="flex-line">
<input
type="radio"
checked={this.state.network === 'none'}
onChange={this.noProxyDidChange}
/>
{nls.localize('arduino/preferences/noProxy', 'No proxy')}
</label>
<label className="flex-line">
<input
type="radio"
checked={this.state.network !== 'none'}
onChange={this.manualProxyDidChange}
/>
{nls.localize(
'arduino/preferences/manualProxy',
'Manual proxy configuration'
)}
</label>
</form>
{this.renderProxySettings()}
</div>
);
}
protected renderProxySettings(): React.ReactNode {
const disabled = this.state.network === 'none';
return (
<Disable disabled={disabled}>
<div className="proxy-settings" aria-disabled={disabled}>
<form className="flex-line">
<input
type="radio"
checked={
this.state.network === 'none'
? true
: this.state.network.protocol === 'http'
}
onChange={this.httpProtocolDidChange}
/>
HTTP
<label className="flex-line">
<input
type="radio"
checked={
this.state.network === 'none'
? false
: this.state.network.protocol !== 'http'
}
onChange={this.socksProtocolDidChange}
/>
SOCKS
</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>
<div className="column stretch">
<div className="flex-line">
<input
className="theia-input stretch with-margin"
type="text"
value={
this.state.network === 'none'
? ''
: this.state.network.hostname
}
onChange={this.hostnameDidChange}
/>
</div>
<div className="flex-line">
<input
className="theia-input small with-margin"
type="number"
pattern="[0-9]"
value={
this.state.network === 'none' ? '' : this.state.network.port
}
onKeyDown={this.numbersOnlyKeyDown}
onChange={this.portDidChange}
/>
</div>
<div className="flex-line">
<input
className="theia-input stretch with-margin"
type="text"
value={
this.state.network === 'none'
? ''
: this.state.network.username
}
onChange={this.usernameDidChange}
/>
</div>
<div className="flex-line">
<input
className="theia-input stretch with-margin"
type="password"
value={
this.state.network === 'none'
? ''
: this.state.network.password
}
onChange={this.passwordDidChange}
/>
</div>
</div>
</div>
</div>
</Disable>
);
}
private isControlKey(event: React.KeyboardEvent<HTMLInputElement>): boolean {
return (
!!event.key &&
['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some(
(key) => event.key.toLocaleLowerCase() === key
)
);
}
protected noopKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
): void => {
if (this.isControlKey(event)) {
return;
}
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
};
protected numbersOnlyKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
): void => {
if (this.isControlKey(event)) {
return;
}
const key = Number(event.key);
if (isNaN(key) || event.key === null || event.key === ' ') {
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
return;
}
};
protected browseSketchbookDidClick = async (): Promise<void> => {
const uri = await this.props.fileDialogService.showOpenDialog({
title: nls.localize(
'arduino/preferences/newSketchbookLocation',
'Select new sketchbook location'
),
openLabel: nls.localize('arduino/preferences/choose', 'Choose'),
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true,
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
this.setState({ sketchbookPath });
}
};
protected editAdditionalUrlDidClick = async (): Promise<void> => {
const additionalUrls = await new AdditionalUrlsDialog(
AdditionalUrls.parse(this.state.rawAdditionalUrlsValue, ','),
this.props.windowService
).open();
if (additionalUrls) {
this.setState({
rawAdditionalUrlsValue: AdditionalUrls.stringify(additionalUrls),
});
}
};
protected editorFontSizeDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
const { value } = event.target;
if (value) {
this.setState({ editorFontSize: parseInt(value, 10) });
}
};
protected rawAdditionalUrlsValueDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
rawAdditionalUrlsValue: event.target.value,
});
};
protected autoScaleInterfaceDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ autoScaleInterface: event.target.checked });
};
protected interfaceScaleDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
const { value } = event.target;
const percentage = parseInt(value, 10);
if (isNaN(percentage)) {
return;
}
const interfaceScale = (percentage - 100) / 20;
if (!isNaN(interfaceScale)) {
this.setState({ interfaceScale });
}
};
protected verifyAfterUploadDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ verifyAfterUpload: event.target.checked });
};
protected sketchbookShowAllFilesDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ sketchbookShowAllFiles: event.target.checked });
};
protected autoSaveDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
autoSave: event.target.checked ? Settings.AutoSave.DEFAULT_ON : 'off',
});
};
protected quickSuggestionsOtherDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
// need to persist react events through lifecycle https://reactjs.org/docs/events.html#event-pooling
const newVal = event.target.checked ? true : false;
this.setState((prevState) => {
return {
quickSuggestions: {
...prevState.quickSuggestions,
other: newVal,
},
};
});
};
protected themeDidChange = (
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
}
};
protected languageDidChange = (
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const selectedLanguage = event.target.value;
this.setState({ currentLanguage: selectedLanguage });
};
protected compilerWarningsDidChange = (
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { selectedIndex } = event.target.options;
const compilerWarnings = CompilerWarningLiterals[selectedIndex];
if (compilerWarnings) {
this.setState({ compilerWarnings });
}
};
protected verboseOnCompileDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ verboseOnCompile: event.target.checked });
};
protected verboseOnUploadDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ verboseOnUpload: event.target.checked });
};
protected sketchpathDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
const sketchbookPath = event.target.value;
if (sketchbookPath) {
this.setState({ sketchbookPath });
}
};
protected noProxyDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (event.target.checked) {
this.setState({ network: 'none' });
} else {
this.setState({ network: Network.Default() });
}
};
protected manualProxyDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (event.target.checked) {
this.setState({ network: Network.Default() });
} else {
this.setState({ network: 'none' });
}
};
protected httpProtocolDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks';
this.setState({ network });
}
};
protected socksProtocolDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks' : 'http';
this.setState({ network });
}
};
protected hostnameDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.hostname = event.target.value;
this.setState({ network });
}
};
protected portDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.port = event.target.value;
this.setState({ network });
}
};
protected usernameDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.username = event.target.value;
this.setState({ network });
}
};
protected passwordDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.password = event.target.value;
this.setState({ network });
}
};
private get cloneProxySettings(): ProxySettings {
const { network } = this.state;
if (network === 'none') {
throw new Error('Must be called when proxy is enabled.');
}
const copyNetwork = deepClone(network);
return copyNetwork;
}
}
export namespace SettingsComponent {
export interface Props {
readonly settingsService: SettingsService;
readonly fileService: FileService;
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
readonly localizationProvider: AsyncLocalizationProvider;
}
export type State = Settings & {
rawAdditionalUrlsValue: string;
};
export namespace State {
export function fromSettings(settings: Settings): State {
return {
...settings,
rawAdditionalUrlsValue: AdditionalUrls.stringify(
settings.additionalUrls
),
};
}
export function toSettings(state: State): Settings {
const parsedAdditionalUrls = AdditionalUrls.parse(
state.rawAdditionalUrlsValue,
','
);
return {
...state,
additionalUrls: AdditionalUrls.sameAs(
state.additionalUrls,
parsedAdditionalUrls
)
? state.additionalUrls
: parsedAdditionalUrls,
};
}
export function merge(prevState: State, settings: Settings): State {
const prevAdditionalUrls = AdditionalUrls.parse(
prevState.rawAdditionalUrlsValue,
','
);
return {
...settings,
rawAdditionalUrlsValue: prevState.rawAdditionalUrlsValue,
additionalUrls: AdditionalUrls.sameAs(
prevAdditionalUrls,
settings.additionalUrls
)
? prevAdditionalUrls
: settings.additionalUrls,
};
}
}
}

View File

@@ -0,0 +1,191 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogError, ReactWidget } from '@theia/core/lib/browser';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser';
import { Settings, SettingsService } from './settings';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { nls } from '@theia/core/lib/common';
import { SettingsComponent } from './settings-component';
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
import { AdditionalUrls } from '../../../common/protocol';
@injectable()
export class SettingsWidget extends ReactWidget {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(AsyncLocalizationProvider)
protected readonly localizationProvider: AsyncLocalizationProvider;
protected render(): React.ReactNode {
return (
<SettingsComponent
settingsService={this.settingsService}
fileService={this.fileService}
fileDialogService={this.fileDialogService}
windowService={this.windowService}
localizationProvider={this.localizationProvider}
/>
);
}
}
@injectable()
export class SettingsDialogProps extends DialogProps {}
@injectable()
export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
constructor(
@inject(SettingsDialogProps)
protected override readonly props: SettingsDialogProps
) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
@postConstruct()
protected init(): void {
this.toDispose.push(
this.settingsService.onDidChange(this.validate.bind(this))
);
}
protected override async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
}
return '';
}
get value(): Promise<Settings> {
return this.settingsService.settings();
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(
this.settingsService.onDidChange(() => this.update())
);
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);
// calling settingsService.reset() in order to reload the settings from the preferenceService
// and update the UI including changes triggered from the command palette
this.settingsService.reset();
this.widget.activate();
}
}
export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
protected readonly textArea: HTMLTextAreaElement;
constructor(urls: string[], windowService: WindowService) {
super({
title: nls.localize(
'arduino/preferences/additionalManagerURLs',
'Additional Boards Manager URLs'
),
});
this.contentNode.classList.add('additional-urls-dialog');
const description = document.createElement('div');
description.textContent = nls.localize(
'arduino/preferences/enterAdditionalURLs',
'Enter additional URLs, one for each row'
);
description.style.marginBottom = '5px';
this.contentNode.appendChild(description);
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)
.join('\n');
this.textArea.wrap = 'soft';
this.textArea.cols = 90;
this.textArea.rows = 5;
this.contentNode.appendChild(this.textArea);
const anchor = document.createElement('div');
anchor.classList.add('link');
anchor.textContent = nls.localize(
'arduino/preferences/unofficialBoardSupport',
'Click for a list of unofficial board support URLs'
);
anchor.style.marginTop = '5px';
anchor.style.cursor = 'pointer';
this.addEventListener(anchor, 'click', () =>
windowService.openNewWindow(
'https://github.com/arduino/Arduino/wiki/Unofficial-list-of-3rd-party-boards-support-urls',
{ external: true }
)
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
}
get value(): string[] {
return AdditionalUrls.parse(this.textArea.value, 'newline');
}
protected override onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected override onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}
return false;
}
}

View File

@@ -0,0 +1,312 @@
import {
injectable,
inject,
postConstruct,
} 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 { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { MaybePromise } from '@theia/core/lib/common/types';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
import {
AdditionalUrls,
CompilerWarnings,
ConfigService,
FileSystemExt,
Network,
} from '../../../common/protocol';
import { CommandService, nls } from '@theia/core/lib/common';
import {
AsyncLocalizationProvider,
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
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 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 ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`;
export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`;
export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`;
export const UPLOAD_VERBOSE_SETTING = `${UPLOAD_SETTING}.verbose`;
export const UPLOAD_VERIFY_SETTING = `${UPLOAD_SETTING}.verify`;
export const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`;
export interface Settings {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: Settings.AutoSave; // `files.autoSave`
quickSuggestions: Record<'other' | 'comments' | 'strings', boolean>; // `editor.quickSuggestions`
languages: (string | LanguageInfo)[]; // `languages from the plugins`
currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
verboseOnCompile: boolean; // `arduino.compile.verbose`
compilerWarnings: CompilerWarnings; // `arduino.compile.warnings`
verboseOnUpload: boolean; // `arduino.upload.verbose`
verifyAfterUpload: boolean; // `arduino.upload.verify`
sketchbookShowAllFiles: boolean; // `arduino.sketchbook.showAllFiles`
sketchbookPath: string; // CLI
additionalUrls: AdditionalUrls; // CLI
network: Network; // CLI
}
export namespace Settings {
export function belongsToCli<K extends keyof Settings>(key: K): boolean {
return key === 'sketchbookPath' || key === 'additionalUrls';
}
export type AutoSave =
| 'off'
| 'afterDelay'
| 'onFocusChange'
| 'onWindowChange';
export namespace AutoSave {
export const DEFAULT_ON: AutoSave = 'afterDelay'; // https://github.com/eclipse-theia/theia/issues/10812
}
}
@injectable()
export class SettingsService {
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@inject(AsyncLocalizationProvider)
protected readonly localizationProvider: AsyncLocalizationProvider;
@inject(CommandService)
protected commandService: CommandService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
readonly onDidReset = this.onDidResetEmitter.event;
protected ready = new Deferred<void>();
protected _settings: Settings;
@postConstruct()
protected async init(): Promise<void> {
const settings = await this.loadSettings();
this._settings = deepClone(settings);
this.ready.resolve();
}
protected async loadSettings(): Promise<Settings> {
await this.preferenceService.ready;
const [
languages,
currentLanguage,
editorFontSize,
themeId,
autoSave,
quickSuggestions,
autoScaleInterface,
interfaceScale,
verboseOnCompile,
compilerWarnings,
verboseOnUpload,
verifyAfterUpload,
sketchbookShowAllFiles,
cliConfig,
] = await Promise.all([
['en', ...(await this.localizationProvider.getAvailableLanguages())],
this.localizationProvider.getCurrentLanguage(),
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
this.preferenceService.get<string>(
'workbench.colorTheme',
'arduino-theme'
),
this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING,
Settings.AutoSave.DEFAULT_ON
),
this.preferenceService.get<
Record<'other' | 'comments' | 'strings', boolean>
>(QUICK_SUGGESTIONS_SETTING, {
other: false,
comments: false,
strings: false,
}),
this.preferenceService.get<boolean>(AUTO_SCALE_SETTING, true),
this.preferenceService.get<number>(ZOOM_LEVEL_SETTING, 0),
this.preferenceService.get<boolean>(COMPILE_VERBOSE_SETTING, true),
this.preferenceService.get<any>(COMPILE_WARNINGS_SETTING, 'None'),
this.preferenceService.get<boolean>(UPLOAD_VERBOSE_SETTING, true),
this.preferenceService.get<boolean>(UPLOAD_VERIFY_SETTING, true),
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
this.configService.getConfiguration(),
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
themeId,
languages,
currentLanguage,
autoSave,
quickSuggestions,
autoScaleInterface,
interfaceScale,
verboseOnCompile,
compilerWarnings,
verboseOnUpload,
verifyAfterUpload,
sketchbookShowAllFiles,
additionalUrls,
sketchbookPath,
network,
};
}
async settings(): Promise<Settings> {
await this.ready.promise;
return this._settings;
}
async update(settings: Settings, fireDidChange = false): Promise<void> {
await this.ready.promise;
for (const key of Object.keys(settings)) {
if (key in this._settings) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._settings as any)[key] = (settings as any)[key];
}
}
if (fireDidChange) {
this.onDidChangeEmitter.fire(this._settings);
}
}
async reset(): Promise<void> {
const settings = await this.loadSettings();
await this.update(settings, false);
this.onDidResetEmitter.fire(this._settings);
}
async validate(
settings: MaybePromise<Settings> = this.settings()
): Promise<string | true> {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
return nls.localize(
'arduino/preferences/invalid.sketchbook.location',
'Invalid sketchbook location: {0}',
sketchbookPath
);
}
if (editorFontSize <= 0) {
return nls.localize(
'arduino/preferences/invalid.editorFontSize',
'Invalid editor font size. It must be a positive integer.'
);
}
if (
!ThemeService.get()
.getThemes()
.find(({ id }) => id === themeId)
) {
return nls.localize(
'arduino/preferences/invalid.theme',
'Invalid theme.'
);
}
return true;
} catch (err) {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
}
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> {
await this.ready.promise;
const {
currentLanguage,
editorFontSize,
themeId,
autoSave,
quickSuggestions,
autoScaleInterface,
interfaceScale,
verboseOnCompile,
compilerWarnings,
verboseOnUpload,
verifyAfterUpload,
sketchbookPath,
additionalUrls,
network,
sketchbookShowAllFiles,
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath),
]);
(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);
this.onDidChangeEmitter.fire(this._settings);
// after saving all the settings, if we need to change the language we need to perform a reload
// Only reload if the language differs from the current locale. `nls.locale === undefined` signals english as well
if (
currentLanguage !== (await this.localizationProvider.getCurrentLanguage())
) {
await this.localizationProvider.setCurrentLanguage(currentLanguage);
if (currentLanguage === 'en') {
window.localStorage.removeItem(nls.localeId);
} else {
window.localStorage.setItem(nls.localeId, currentLanguage);
}
this.commandService.executeCommand(ElectronCommands.RELOAD.id);
}
return true;
}
}

View File

@@ -0,0 +1,98 @@
import * as React from '@theia/core/shared/react';
import { BoardUserField } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';
export const UserFieldsComponent = ({
initialBoardUserFields,
updateUserFields,
cancel,
accept,
}: {
initialBoardUserFields: BoardUserField[];
updateUserFields: (userFields: BoardUserField[]) => void;
cancel: () => void;
accept: () => Promise<void>;
}): React.ReactElement => {
const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[]
>(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true);
React.useEffect(() => {
setBoardUserFields(initialBoardUserFields);
}, [initialBoardUserFields]);
const updateUserField =
(index: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const newBoardUserFields = [...boardUserFields];
newBoardUserFields[index].value = e.target.value;
setBoardUserFields(newBoardUserFields);
};
const allFieldsHaveValues = (userFields: BoardUserField[]): boolean => {
return (
userFields &&
userFields.length > 0 &&
userFields
.map<boolean>((field: BoardUserField): boolean => {
return field.value.length > 0;
})
.reduce((previous: boolean, current: boolean): boolean => {
return previous && current;
})
);
};
React.useEffect(() => {
updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
}, [boardUserFields]);
return (
<div>
<div className="user-fields-container">
<div className="user-fields-list">
{boardUserFields.map((field, index) => {
return (
<div className="dialogSection" key={index}>
<div className="dialogRow">
<label className="field-label">{field.label}</label>
</div>
<div className="dialogRow">
<input
type={field.secret ? 'password' : 'text'}
value={field.value}
className="theia-input"
placeholder={'Enter ' + field.label}
onChange={updateUserField(index)}
/>
</div>
</div>
);
})}
</div>
</div>
<div className="dialogSection">
<div className="dialogRow button-container">
<button
type="button"
className="theia-button secondary install-cert-btn"
onClick={cancel}
>
{nls.localize('arduino/userFields/cancel', 'Cancel')}
</button>
<button
type="button"
className="theia-button primary install-cert-btn"
disabled={uploadButtonDisabled}
onClick={accept}
>
{nls.localize('arduino/userFields/upload', 'Upload')}
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
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 { 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>
);
}
}
@injectable()
export class UserFieldsDialogProps extends DialogProps {}
@injectable()
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
protected readonly widget: UserFieldsDialogWidget;
constructor(
@inject(UserFieldsDialogProps)
protected override readonly props: UserFieldsDialogProps
) {
super({
title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '',
});
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;
}
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
for (const field of this.value) {
if (field.value.length === 0) {
return;
}
}
return super.accept();
}
override close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
super.close();
}
}

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution,
FrontendApplication,

View File

@@ -0,0 +1,74 @@
import { DisposableCollection, Emitter, Event } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
/**
* Frontend contribution to watch VS Code extension start/stop events from Theia.
*
* In Theia, there are no events when a VS Code extension is loaded, started, unloaded, and stopped.
* Currently, it's possible to `@inject` the `HostedPluginSupport` service from Theia and `await`
* for the `didStart` promise to resolve. But if the OS goes to sleep, the VS Code extensions will
* be unloaded and loaded and started again when the OS awakes. Theia reloads the VS Code extensions
* after the OS awake event, but the `didStart` promise was already resolved, so IDE2 cannot restart the LS.
* This service is meant to work around the limitation of Theia and fire an event every time the VS Code extensions
* loaded and started.
*/
@injectable()
export class HostedPluginEvents implements FrontendApplicationContribution {
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
private firstStart = true;
private readonly onPluginsDidStartEmitter = new Emitter<void>();
private readonly onPluginsWillUnloadEmitter = new Emitter<void>();
private readonly toDispose = new DisposableCollection(
this.onPluginsDidStartEmitter,
this.onPluginsWillUnloadEmitter
);
onStart(): void {
this.hostedPluginSupport.onDidLoad(() => {
// Fire the first event, when `didStart` resolves.
if (!this.firstStart) {
console.debug('HostedPluginEvents', "Received 'onDidLoad' event.");
this.onPluginsDidStartEmitter.fire();
} else {
console.debug(
'HostedPluginEvents',
"Received 'onDidLoad' event before the first start. Skipping."
);
}
});
this.hostedPluginSupport.didStart.then(() => {
console.debug('HostedPluginEvents', "Hosted plugins 'didStart'.");
if (!this.firstStart) {
throw new Error(
'Unexpectedly received a `didStart` event after the first start.'
);
}
this.firstStart = false;
this.onPluginsDidStartEmitter.fire();
});
this.hostedPluginSupport.onDidCloseConnection(() => {
console.debug('HostedPluginEvents', "Received 'onDidCloseConnection'.");
this.onPluginsWillUnloadEmitter.fire();
});
}
onStop(): void {
this.toDispose.dispose();
}
get onPluginsDidStart(): Event<void> {
return this.onPluginsDidStartEmitter.event;
}
get onPluginsWillUnload(): Event<void> {
return this.onPluginsWillUnloadEmitter.event;
}
get didStart(): Promise<void> {
return this.hostedPluginSupport.didStart;
}
}

View File

@@ -1,4 +1,7 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg width="28" height="28" viewBox="0 0 28 28" fill="#F6F6F6" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M15.1673 18.0687V23.0247C15.1673 23.5637 15.2723 24.5 14.7315 24.5H12.8328V23.3327H14V19.6122L13.7988 19.4022C13.0604 20.0803 12.1008 20.4669 11.0986 20.49C10.0964 20.5132 9.11994 20.1714 8.351 19.5282C7 18.1737 7.13826 16.3327 8.60475 14H4.66726V15.1672H3.50001V13.2685C3.50001 12.7277 4.43626 12.8327 4.97526 12.8327H9.76326L15.1673 18.0687ZM11.6673 5.83275H10.5V4.66725H12.775C13.3123 4.66725 14 4.9245 14 5.4635V9.366L14.8593 10.3862C14.927 9.83979 15.1906 9.33644 15.6013 8.96958C16.0119 8.60271 16.5416 8.39723 17.0923 8.39125C17.2298 8.37945 17.3684 8.39492 17.5 8.43675V5.83275H18.6673V8.88825C18.703 8.99154 18.7618 9.08536 18.8391 9.16265C18.9164 9.23995 19.0102 9.29871 19.1135 9.3345H22.1673V10.5H19.5615C19.593 10.5 19.6105 10.675 19.6105 10.85C19.6058 11.4034 19.4011 11.9365 19.0341 12.3508C18.6671 12.7651 18.1626 13.0326 17.6138 13.104L18.634 14H22.5383C23.0773 14 23.3345 14.6807 23.3345 15.225V17.5H22.1673V16.3327H19.2273L11.6673 8.98275V5.83275ZM14 0C11.2311 0 8.52431 0.821086 6.22202 2.35943C3.91973 3.89776 2.12532 6.08426 1.06569 8.64243C0.00606593 11.2006 -0.271181 14.0155 0.269012 16.7313C0.809205 19.447 2.14258 21.9416 4.10051 23.8995C6.05845 25.8574 8.55301 27.1908 11.2687 27.731C13.9845 28.2712 16.7994 27.9939 19.3576 26.9343C21.9157 25.8747 24.1022 24.0803 25.6406 21.778C27.1789 19.4757 28 16.7689 28 14C28 10.287 26.525 6.72601 23.8995 4.1005C21.274 1.475 17.713 0 14 0V0ZM25.6673 14C25.6692 16.6908 24.7364 19.2988 23.0283 21.378L6.622 4.97175C8.33036 3.57269 10.4009 2.68755 12.5927 2.41935C14.7845 2.15115 17.0074 2.51091 19.0027 3.45676C20.998 4.40262 22.6836 5.89567 23.8635 7.76217C25.0433 9.62867 25.6689 11.7919 25.6673 14ZM2.33276 14C2.33066 11.3091 3.26351 8.70111 4.97176 6.622L21.378 23.03C19.6693 24.4284 17.5987 25.313 15.407 25.5807C13.2153 25.8485 10.9926 25.4884 8.99754 24.5425C7.00244 23.5965 5.31693 22.1036 4.13708 20.2373C2.95722 18.3709 2.33152 16.208 2.33276 14Z" fill="white"/></g><defs><clipPath id="clip0"><rect width="28" height="28" fill="#F6F6F6"/></clipPath></defs></svg>
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6259 14.2462C13.4329 14.2464 13.2452 14.1827 13.0922 14.0652C12.9391 13.9476 12.8292 13.7827 12.7796 13.5962C12.73 13.4097 12.7434 13.212 12.8178 13.034C12.8922 12.8559 13.0234 12.7074 13.1909 12.6116L19.7355 8.87405L9.25 2.88232V9.25C9.25 9.48206 9.15781 9.70462 8.99372 9.86872C8.82962 10.0328 8.60706 10.125 8.375 10.125C8.14294 10.125 7.92038 10.0328 7.75628 9.86872C7.59219 9.70462 7.5 9.48206 7.5 9.25V1.375C7.49991 1.22171 7.54012 1.07108 7.61658 0.938223C7.69304 0.805362 7.80308 0.694931 7.93567 0.617993C8.06825 0.541055 8.21873 0.500314 8.37202 0.499851C8.52531 0.499389 8.67603 0.539221 8.80908 0.615357L21.9341 8.11438C22.068 8.19089 22.1793 8.30145 22.2568 8.43486C22.3342 8.56826 22.375 8.71977 22.375 8.87402C22.375 9.02827 22.3342 9.17978 22.2568 9.31319C22.1793 9.4466 22.068 9.55716 21.9341 9.63367L14.0591 14.1309C13.9273 14.2066 13.7779 14.2464 13.6259 14.2462Z" fill="#BDC7C7"/>
<path d="M11 17.125C11.2321 17.125 11.4546 17.0328 11.6187 16.8687C11.7828 16.7046 11.875 16.4821 11.875 16.25C11.875 16.0179 11.7828 15.7954 11.6187 15.6313C11.4546 15.4672 11.2321 15.375 11 15.375H10.0724C10.0297 15.0014 9.95355 14.6325 9.84495 14.2725L10.7462 13.3712C10.8278 13.2897 10.8925 13.1928 10.9367 13.0862C10.9808 12.9796 11.0035 12.8654 11.0035 12.75C11.0035 12.6346 10.9808 12.5204 10.9367 12.4138C10.8925 12.3072 10.8278 12.2104 10.7462 12.1288C10.6646 12.0472 10.5678 11.9825 10.4612 11.9383C10.3546 11.8942 10.2404 11.8714 10.125 11.8714C10.0096 11.8714 9.89537 11.8942 9.78878 11.9383C9.68219 11.9825 9.58533 12.0472 9.50375 12.1288L9.04 12.5925C8.72733 12.1175 8.30488 11.7248 7.80837 11.4477C7.31186 11.1705 6.75589 11.0169 6.1875 11C5.6191 11.0169 5.06314 11.1705 4.56663 11.4477C4.07011 11.7249 3.64766 12.1176 3.335 12.5926L2.87125 12.1288C2.70649 11.964 2.48303 11.8715 2.25002 11.8715C2.01701 11.8715 1.79355 11.964 1.62878 12.1288C1.46401 12.2935 1.37145 12.517 1.37144 12.75C1.37144 12.983 1.46399 13.2065 1.62875 13.3712L2.53 14.2725C2.4214 14.6325 2.34526 15.0014 2.3025 15.375H1.375C1.14294 15.375 0.920376 15.4672 0.756282 15.6313C0.592187 15.7954 0.5 16.0179 0.5 16.25C0.5 16.4821 0.592187 16.7046 0.756282 16.8687C0.920376 17.0328 1.14294 17.125 1.375 17.125H2.30255C2.34532 17.4986 2.42145 17.8675 2.53005 18.2275L2.5038 18.2538L1.6288 19.1288C1.46485 19.2939 1.37285 19.5172 1.37285 19.75C1.37285 19.9827 1.46485 20.206 1.6288 20.3712C1.79466 20.5338 2.01771 20.625 2.25002 20.625C2.48233 20.625 2.70537 20.5338 2.87123 20.3712L3.33498 19.9074C3.64765 20.3824 4.0701 20.7751 4.56661 21.0523C5.06313 21.3295 5.6191 21.4831 6.1875 21.5C6.7559 21.4831 7.31186 21.3295 7.80837 21.0523C8.30489 20.7751 8.72734 20.3824 9.04 19.9074L9.50375 20.3712C9.66961 20.5339 9.89265 20.625 10.125 20.625C10.3573 20.625 10.5803 20.5339 10.7462 20.3712C10.9101 20.206 11.0021 19.9827 11.0021 19.75C11.0021 19.5172 10.9101 19.2939 10.7462 19.1288L9.87118 18.2538L9.84493 18.2275C9.95353 17.8675 10.0297 17.4986 10.0724 17.125L11 17.125ZM6.1875 12.75C6.98367 12.75 7.68375 13.4588 8.06875 14.5H4.30625C4.69125 13.4588 5.39133 12.75 6.1875 12.75ZM6.1875 19.75C4.9975 19.75 4 18.1487 4 16.25H8.375C8.375 18.1487 7.3775 19.75 6.1875 19.75Z" fill="#BDC7C7"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

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