Compare commits

...

73 Commits

Author SHA1 Message Date
Alberto Iannaccone
439cdfbbff 2.0.0-rc8 (#1105) 2022-06-23 11:33:27 +02:00
Alberto Iannaccone
672fd4e4b0 bump arduino-cli version to 0.24.0 (#1103) 2022-06-23 10:59:07 +02:00
David Simpson
0f1d379e58 reference cli rc1 0.24 in package.json (#1098) 2022-06-22 18:54:30 +02:00
Francesco Spissu
a79c9b4449 Sketchbook tree indentation (#1097) 2022-06-22 18:23:14 +02:00
Akos Kitta
0f8a29a493 Disabled MenuItem roles on macOS.
Closes #969
Upstream-ref: eclipse-theia/theia#11217

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

* move upload failure fix logic to frontend

* misc corrections

* avoid starting monitor when upload is in progress

* avoid starting monitor when upload is in progress

* prevent monitor side effects on upload (WIP)

* send upload req after notifying mgr

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

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

This reverts commit 2d5dff2a2d.

* force wait before upload (test)

* always start queued services after uplaod finishes

* test cli with monitor close delay

* clean up unnecessary await(s)

* remove unused dependency

* revert CLI to 0.23

* use master cli for testing, await in upload finish

* remove upload port from pending monitor requests

* fix startQueuedServices

* refinements queued monitors

* clean up monitor mgr state

* fix typo from prev cleanup

* avoid dupl queued monitor services

* variable name changes

* reference latest cli commit in package.json

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

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

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

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

Closes #1014.

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

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

* create buffer provider

* output panel buffer corrections

* output buffer cleanup

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

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

Closes #1036

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

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

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

* Scaffold interfaces and classes for pluggable monitors

* Implement MonitorService to handle pluggable monitor lifetime

* Rename WebSocketService to WebSocketProvider and uninjected it

* Moved some interfaces

* Changed upload settings

* Enhance MonitorManager APIs

* Fixed WebSocketChange event signature

* Add monitor proxy functions for the frontend

* Moved settings to MonitorService

* Remove several unnecessary serial monitor classes

* Changed how connection is handled on upload

* Proxied more monitor methods to frontend

* WebSocketProvider is not injectable anymore

* Add generic monitor settings storaging

* More serial classes removal

* Remove unused file

* Changed plotter contribution to use new manager proxy

* Changed MonitorWidget and children to use new monitor proxy

* Updated MonitorWidget to use new monitor proxy

* Fix backend logger bindings

* Delete unnecessary Symbol

* coreClientProvider is now set when constructing MonitorService

* Add missing binding

* Fix `MonitorManagerProxy` DI issue

* fix monitor connection

* delete duplex when connection is closed

* update arduino-cli to 0.22.0

* fix upload when monitor is open

* add MonitorSettingsProvider interface

* monitor settings provider stub

* updated pseudo code

* refactor monitor settings interfaces

* monitor service provider singleton

* add unit tests

* change MonitorService providers to injectable deps

* fix monitor settings client communication

* refactor monitor commands protocol

* use monitor settings provider properly

* add settings to monitor model

* add settings to monitor model

* reset serial monitor when port changes

* fix serial plotter opening

* refine monitor connection settings

* fix hanging web socket connections

* add serial plotter reset command

* send port to web socket clients

* monitor service wait for success serial port open

* fix reset loop

* update serial plotter version

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

* remove useless plotter protocol file

* localize web socket errors

* clean-up code

* update translation file

* Fix duplicated editor tabs (#1012)

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

* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

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

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

* Fixed the translations.

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

* Fixed the translations again.

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

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

* Aligned the stop handler code to Theia.

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

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

* fix serial monitor send line ending

* refactor monitor-service poll for test/readability

* localize web socket errors

* update translation file

* Fix duplicated editor tabs (#1012)

* i18n:check rerun

* Speed up IDE startup time.

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

* override coreClientProvider in monitor-service

* cleanup merged code

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

* 944: Prevent race conditions setting authOptions

* typo correction, duplicate identifier

* prevent block of auth client service on setOptions

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

Closes #1009
Closes #566

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

* Align `CLOSE` command to rest of app

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

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

* Fixed the translations.

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

* Fixed the translations again.

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

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

* Aligned the stop handler code to Theia.

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

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

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

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

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

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

Those issues are resolved by the following changes:

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

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

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

* register localization contribution to backend module

* copy i18n folder to build

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

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

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

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

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

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

The behavior of the NSIS installer is unchanged:

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

Closes #919.

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

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

Closes #881.

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

* rephrase notes for Windows contributor in BUILDING.md

* Update notes for Windows contributor in BUILDING.md

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

* move Notes for Windows contributors in Build from source section

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

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

- bug report
- feature request

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

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

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

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

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

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

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

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

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

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

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

* fix how the old changelog is cut off
2022-03-14 12:05:53 +01:00
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
333 changed files with 26031 additions and 14446 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: imperfection'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Version: [e.g. 2.0.0]
**Additional context**
Add any other context about the problem here.

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

@@ -78,6 +78,7 @@ jobs:
fi
fi
npx node-gyp install
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package

View File

@@ -2,10 +2,13 @@ name: Compose full changelog
on:
release:
types: [created, edited]
types:
- edited
env:
CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 14.x
jobs:
create-changelog:
@@ -15,6 +18,12 @@ jobs:
- 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: |
@@ -23,10 +32,11 @@ jobs:
- name: Create full changelog
id: full-changelog
run: |
yarn add @octokit/rest --ignore-workspace-root-check
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
# Get the changelog file name to build
CHANGELOG_FILE_NAME="${{ steps.tag_name.outputs.TAG_NAME }}-$(date --iso-8601=s).md"
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"

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>

6
.gitignore vendored
View File

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

View File

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

40
.vscode/launch.json vendored
View File

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

View File

@@ -66,6 +66,13 @@ Once you have all the tools installed, you can build the editor following these
yarn start
```
### Notes for Windows contributors
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
### CI
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
@@ -82,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.

View File

@@ -23,13 +23,14 @@ should be considered unstable:
| Platform | 32 bit | 64 bit |
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
| Linux | | [Nightly Linux 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 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

View File

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

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

@@ -66,21 +66,24 @@
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, lsSuffix, clangdSuffix;
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
@@ -103,4 +106,15 @@
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
clangFormatExecutablePath,
force,
{
strip: 1,
}
);
})();

View File

@@ -1,5 +1,9 @@
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
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,
@@ -7,6 +11,7 @@ import {
ExecutableService,
Sketch,
LibraryService,
ArduinoDaemon,
} from '../common/protocol';
import { Mutex } from 'async-mutex';
import {
@@ -17,9 +22,12 @@ import {
DisposableCollection,
} from '@theia/core';
import {
Dialog,
FrontendApplication,
FrontendApplicationContribution,
LocalStorageService,
OnWillStopAction,
SaveableWidget,
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser';
@@ -38,23 +46,17 @@ 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 { 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 { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ConfigService } from '../common/protocol/config-service';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
@@ -65,11 +67,15 @@ import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
import { IDEUpdater } from '../common/protocol/ide-updater';
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';
@@ -81,89 +87,73 @@ export class ArduinoFrontendContribution
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution {
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(LibraryService)
protected readonly libraryService: LibraryService;
private readonly libraryService: LibraryService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
private readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
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;
private readonly boardsConfigDialog: BoardsConfigDialog;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SketchbookWidgetContribution)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
private readonly statusBar: StatusBar;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
private readonly editorMode: EditorMode;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
protected executableService: ExecutableService;
private readonly executableService: ExecutableService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
private readonly sketchServiceClient: SketchesServiceClientImpl;
protected readonly appStateService: FrontendApplicationStateService;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
private readonly localStorageService: LocalStorageService;
@inject(FileSystemFrontendContribution)
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
@inject(IDEUpdater)
protected readonly updater: IDEUpdater;
private readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
protected readonly updaterDialog: IDEUpdaterDialog;
private readonly updaterDialog: IDEUpdaterDialog;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
protected invalidConfigPopup:
| Promise<void | 'No' | 'Yes' | undefined>
@@ -234,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) => {
@@ -260,21 +253,6 @@ export class ArduinoFrontendContribution
}
async onStart(app: FrontendApplication): Promise<void> {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
this.updater
.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
@@ -308,6 +286,12 @@ 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.newValue !== event.oldValue) {
switch (event.preferenceName) {
@@ -337,6 +321,19 @@ export class ArduinoFrontendContribution
});
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();
}
}
}
);
}
onStop(): void {
@@ -349,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.
@@ -386,7 +387,7 @@ 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)
);
@@ -398,8 +399,6 @@ export class ArduinoFrontendContribution
this.fileService.fsPath(new URI(lsUri)),
]);
const config = await this.configService.getConfiguration();
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
@@ -411,7 +410,7 @@ export class ArduinoFrontendContribution
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${config.daemon.port}`, // TODO: verify if this port is coming from the BE
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
@@ -469,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];
@@ -540,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,
}
);
}
}
@@ -629,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 { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { EditorPreviewContribution } from './theia/editor/editor-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry,
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 { SerialServiceClientImpl } from './serial/serial-service-client-impl';
import {
SerialServicePath,
SerialService,
SerialServiceClient,
} from '../common/protocol/serial-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { SerialConnectionManager } from './serial/serial-connection-manager';
import { SerialModel } from './serial/serial-model';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
@@ -130,6 +121,7 @@ import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { SurveyNotification } from './contributions/survey-notification';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
import { OpenSketchExternal } from './contributions/open-sketch-external';
@@ -138,7 +130,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';
@@ -160,7 +151,14 @@ 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';
@@ -275,20 +273,51 @@ import {
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
const ElementQueries = require('css-element-queries/src/ElementQueries');
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
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);
@@ -405,31 +434,47 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(CoreErrorHandler).toSelf().inSingletonScope();
// Serial monitor
bind(SerialModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SerialModel);
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
}));
// Frontend binding for the serial service
bind(SerialService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get<SerialServiceClient>(
SerialServiceClient
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
return connection.createProxy(SerialServicePath, client);
})
.inSingletonScope();
bind(SerialConnectionManager).toSelf().inSingletonScope();
},
}));
// Serial service client to receive and delegate notifications from the backend.
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
bind(MonitorManagerProxyFactory).toFactory(
(context) => () =>
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
);
bind(MonitorManagerProxy)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
MonitorManagerProxyPath,
context.container.get(MonitorManagerProxyClient)
)
)
.inSingletonScope();
// Monitor manager proxy client to receive and delegate pluggable monitors
// notifications from the backend
bind(MonitorManagerProxyClient)
.to(MonitorManagerProxyClientImpl)
.inSingletonScope();
bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
@@ -442,6 +487,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
// Survey notification
bind(SurveyNotification).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SurveyNotification);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution)
@@ -453,9 +511,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaKeymapsFrontendContribution)
.to(KeymapsFrontendContribution)
.inSingletonScope();
rebind(TheiaEditorPreviewContribution)
.to(EditorPreviewContribution)
.inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaMonacoStatusBarContribution)
.to(MonacoStatusBarContribution)
.inSingletonScope();
@@ -477,7 +533,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaPreferencesContribution)
.to(PreferencesContribution)
.inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution)
.to(WorkspaceCommandContribution)
.inSingletonScope();
@@ -486,11 +541,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) => () => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
@@ -508,6 +564,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).to(EditorManager);
// replace search icon
rebind(TheiaSearchInWorkspaceFactory)
.to(SearchInWorkspaceFactory)
@@ -550,6 +610,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
// To remove `New Window` from the `File` menu
bind(WindowContribution).toSelf().inSingletonScope();
rebind(TheiaWindowContribution).toService(WindowContribution);
bind(ArduinoDaemon)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -559,6 +623,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
.inSingletonScope();
bind(Formatter)
.toDynamicValue(({ container }) =>
WebSocketConnectionProvider.createProxy(container, FormatterPath)
)
.inSingletonScope();
bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -626,6 +696,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
// 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()
@@ -655,15 +734,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Enable the dirty indicator on uncloseable widgets.
rebind(TabBarRendererFactory).toFactory((context) => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(
ContextMenuRenderer
);
const contextMenuRenderer =
context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(
TabBarDecoratorService
);
const iconThemeService = context.container.get<IconThemeService>(
IconThemeService
);
const iconThemeService =
context.container.get<IconThemeService>(IconThemeService);
return new TabBarRenderer(
contextMenuRenderer,
decoratorService,
@@ -673,6 +750,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
@@ -680,6 +759,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
// Do not fetch the `catalog.json` from Azure on FE load.
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
rebind(TheiaDefaultJsonSchemaContribution).toService(
DefaultJsonSchemaContribution
);
// Do not block the app startup when initializing the editor navigation history.
bind(EditorNavigationContribution).toSelf().inSingletonScope();
rebind(TheiaEditorNavigationContribution).toService(
EditorNavigationContribution
);
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
@@ -692,6 +791,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// Preferences
bindArduinoPreferences(bind);
@@ -799,4 +902,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
})
.inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
});

View File

@@ -1,4 +1,4 @@
import { interfaces } from 'inversify';
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
@@ -13,6 +13,32 @@ export enum UpdateChannel {
Stable = 'stable',
Nightly = 'nightly',
}
export const ErrorRevealStrategyLiterals = [
/**
* Scroll vertically as necessary and reveal a line.
*/
'auto',
/**
* Scroll vertically as necessary and reveal a line centered vertically.
*/
'center',
/**
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
*/
'top',
/**
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
}
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
@@ -33,6 +59,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/compile.revealRange',
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
@@ -83,7 +126,7 @@ export const ArduinoConfigSchema: PreferenceSchema = {
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
`The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'`
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
@@ -174,12 +217,30 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
},
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
@@ -198,6 +259,8 @@ export interface ArduinoConfiguration {
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

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

View File

@@ -1,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 {

View File

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

View File

@@ -1,5 +1,5 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
@@ -26,7 +26,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
constructor(
@inject(BoardsConfigDialogProps)
protected readonly props: BoardsConfigDialogProps
protected override readonly props: BoardsConfigDialogProps
) {
super(props);
@@ -52,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') {
@@ -95,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);
}
@@ -110,23 +110,23 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
this.update();
}
protected onUpdateRequest(msg: Message) {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
@@ -16,6 +16,7 @@ import {
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
@@ -29,6 +30,7 @@ export namespace BoardsConfig {
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
@@ -47,7 +49,7 @@ export abstract class Item<T> extends React.Component<{
missing?: boolean;
details?: string;
}> {
render(): React.ReactNode {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
@@ -99,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,
@@ -141,11 +147,11 @@ export class BoardsConfig extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
@@ -250,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))}

View File

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

View File

@@ -1,4 +1,4 @@
import { injectable, inject, named } from 'inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { Event, Emitter } from '@theia/core/lib/common/event';

View File

@@ -1,4 +1,4 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
BoardsPackage,
BoardsService,
@@ -30,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(() =>
@@ -42,7 +42,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -63,7 +63,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
);
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {

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';

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';
@@ -41,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);
}
@@ -130,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();
}
@@ -161,7 +161,7 @@ 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, {

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

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
@@ -28,21 +28,17 @@ export class AddZipLibrary extends SketchContribution {
@inject(LibraryService)
protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -47,7 +47,7 @@ export class BoardSelection extends SketchContribution {
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } =
@@ -100,19 +100,20 @@ PID: ${PID}`;
});
}
onStart(): void {
override onStart(): void {
this.notificationCenter.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)
);
this.boardsServiceProvider.onAvailablePortsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {

View File

@@ -1,12 +1,9 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { CoreService } from '../../common/protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -14,29 +11,20 @@ import {
import { nls } from '@theia/core/lib/common';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
export class BurnBootloader extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected 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: nls.localize(
@@ -60,9 +48,15 @@ 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,
@@ -78,15 +72,7 @@ export class BurnBootloader extends SketchContribution {
}
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
await this.serialConnection.reconnectAfterUpload();
this.handleError(e);
}
}
}

View File

@@ -1,12 +1,10 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
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,
@@ -23,90 +21,21 @@ import { nls } from '@theia/core/lib/common';
@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: [
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.dontSave',
"Don't Save"
),
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.save',
'Save'
),
],
message: nls.localize(
'arduino/common/saveChangesToSketch',
'Do you want to save changes to this sketch before closing?'
),
detail: nls.localize(
'arduino/common/loseChanges',
"If you don't save, your changes will be lost."
),
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
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: nls.localize('vscode/editor.contribution/close', 'Close'),
@@ -114,7 +43,7 @@ export class Close extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',

View File

@@ -0,0 +1,656 @@
import {
Command,
CommandRegistry,
Disposable,
DisposableCollection,
Emitter,
MaybePromise,
nls,
notEmpty,
} from '@theia/core';
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Location,
Range,
} from '@theia/core/shared/vscode-languageserver-protocol';
import {
EditorWidget,
TextDocumentChangeEvent,
} from '@theia/editor/lib/browser';
import {
EditorDecoration,
TrackedRangeStickiness,
} from '@theia/editor/lib/browser/decorations/editor-decoration';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import * as monaco from '@theia/monaco-editor-core';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
import { CoreError } from '../../common/protocol/core-service';
import {
ArduinoPreferences,
ErrorRevealStrategy,
} from '../arduino-preferences';
import { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { Contribution } from './contribution';
import { CoreErrorHandler } from './core-error-handler';
interface ErrorDecoration {
/**
* This is the unique ID of the decoration given by `monaco`.
*/
readonly id: string;
/**
* The resource this decoration belongs to.
*/
readonly uri: string;
}
namespace ErrorDecoration {
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
): Promise<monaco.Range | undefined>;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: MonacoEditor
): monaco.Range | undefined;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider:
| ((uri: string) => Promise<MonacoEditor | undefined>)
| MonacoEditor
): MaybePromise<monaco.Range | undefined> {
if (editorProvider instanceof MonacoEditor) {
const control = editorProvider.getControl();
const model = control.getModel();
if (model) {
return control
.getDecorationsInRange(fullRange(model))
?.find(({ id: candidateId }) => id === candidateId)?.range;
}
return undefined;
}
return editorProvider(uri).then((editor) => {
if (editor) {
return rangeOf({ id, uri }, editor);
}
return undefined;
});
}
// export async function rangeOf(
// { id, uri }: ErrorDecoration,
// editorProvider:
// | ((uri: string) => Promise<MonacoEditor | undefined>)
// | MonacoEditor
// ): Promise<monaco.Range | undefined> {
// const editor =
// editorProvider instanceof MonacoEditor
// ? editorProvider
// : await editorProvider(uri);
// if (editor) {
// const control = editor.getControl();
// const model = control.getModel();
// if (model) {
// return control
// .getDecorationsInRange(fullRange(model))
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
// }
// }
// return undefined;
// }
export function sameAs(
left: ErrorDecoration,
right: ErrorDecoration
): boolean {
return left.id === right.id && left.uri === right.uri;
}
}
@injectable()
export class CompilerErrors
extends Contribution
implements monaco.languages.CodeLensProvider
{
@inject(EditorManager)
private readonly editorManager: EditorManager;
@inject(ProtocolToMonacoConverter)
private readonly p2m: ProtocolToMonacoConverter;
@inject(MonacoToProtocolConverter)
private readonly mp2: MonacoToProtocolConverter;
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
@inject(ArduinoPreferences)
private readonly preferences: ArduinoPreferences;
private readonly errors: ErrorDecoration[] = [];
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
private readonly onCurrentErrorDidChange =
this.currentErrorDidChangEmitter.event;
private readonly toDisposeOnCompilerErrorDidChange =
new DisposableCollection();
private shell: ApplicationShell | undefined;
private revealStrategy = ErrorRevealStrategy.Default;
private currentError: ErrorDecoration | undefined;
private get currentErrorIndex(): number {
const current = this.currentError;
if (!current) {
return -1;
}
return this.errors.findIndex((error) =>
ErrorDecoration.sameAs(error, current)
);
}
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
monaco.languages.registerCodeLensProvider(InoSelector, this);
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
);
this.onCurrentErrorDidChange(async (error) => {
const range = await ErrorDecoration.rangeOf(error, (uri) =>
this.monacoEditor(uri)
);
if (!range) {
console.warn(
'compiler-errors',
`Could not find range of decoration: ${error.id}`
);
return;
}
const editor = await this.revealLocationInEditor({
uri: error.uri,
range: this.mp2.asRange(range),
});
if (!editor) {
console.warn(
'compiler-errors',
`Failed to mark error ${error.id} as the current one.`
);
}
});
this.preferences.ready.then(() => {
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.compile.revealRange') {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
}
});
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to next error. Unknown current error.`
);
return;
}
const nextError =
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
this.markAsCurrentError(nextError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to previous error. Unknown current error.`
);
return;
}
const previousError =
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
this.markAsCurrentError(previousError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
}
get onDidChange(): monaco.IEvent<this> {
return this.onDidChangeEmitter.event;
}
async provideCodeLenses(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.CodeLensList> {
const lenses: monaco.languages.CodeLens[] = [];
if (
this.currentError &&
this.currentError.uri === model.uri.toString() &&
this.errors.length > 1
) {
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
this.monacoEditor(uri)
);
if (range) {
lenses.push(
{
range,
command: {
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
title: nls.localize(
'arduino/editor/previousError',
'Previous Error'
),
arguments: [this.currentError],
},
},
{
range,
command: {
id: CompilerErrors.Commands.NEXT_ERROR.id,
title: nls.localize('arduino/editor/nextError', 'Next Error'),
arguments: [this.currentError],
},
}
);
}
}
return {
lenses,
dispose: () => {
/* NOOP */
},
};
}
private async handleCompilerErrorsDidChange(
errors: CoreError.Compiler[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource(
await this.filter(errors)
);
const decorations = await this.decorateEditors(compilerErrorsPerResource);
this.errors.push(...decorations.errors);
this.toDisposeOnCompilerErrorDidChange.pushAll([
Disposable.create(() => (this.errors.length = 0)),
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
...(await Promise.all([
decorations.dispose,
this.trackEditors(
compilerErrorsPerResource,
(editor) =>
editor.editor.onSelectionChanged((selection) =>
this.handleSelectionChange(editor, selection)
),
(editor) =>
editor.onDidDispose(() =>
this.handleEditorDidDispose(editor.editor.uri.toString())
),
(editor) =>
editor.editor.onDocumentContentChanged((event) =>
this.handleDocumentContentChange(editor, event)
)
),
])),
]);
const currentError = this.errors[0];
if (currentError) {
await this.markAsCurrentError(currentError);
}
}
private async filter(
errors: CoreError.Compiler[]
): Promise<CoreError.Compiler[]> {
if (!errors.length) {
return [];
}
await this.preferences.ready;
if (this.preferences['arduino.compile.experimental']) {
return errors;
}
// Always shows maximum one error; hence the code lens navigation is unavailable.
return [errors[0]];
}
private async decorateEditors(
errors: Map<string, CoreError.Compiler[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) =>
this.decorateEditor(uri, errors)
)
);
return {
dispose: new DisposableCollection(
...composite.map(({ dispose }) => dispose)
),
errors: composite.reduce(
(acc, { errors }) => acc.concat(errors),
[] as ErrorDecoration[]
),
};
}
private async decorateEditor(
uri: string,
errors: CoreError.Compiler[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
return { dispose: Disposable.NULL, errors: [] };
}
const oldDecorations = editor.editor.deltaDecorations({
oldDecorations: [],
newDecorations: errors.map((error) =>
this.compilerErrorDecoration(error.location.range)
),
});
return {
dispose: Disposable.create(() => {
if (editor) {
editor.editor.deltaDecorations({
oldDecorations,
newDecorations: [],
});
}
}),
errors: oldDecorations.map((id) => ({ id, uri })),
};
}
private compilerErrorDecoration(range: Range): EditorDecoration {
return {
range,
options: {
isWholeLine: true,
className: 'compiler-error',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
},
};
}
/**
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
*/
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
const uri = monacoEditor.uri.toString();
const monacoSelection = this.p2m.asRange(selection);
console.log(
'compiler-errors',
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
);
const calculatePriority = (
candidateErrorRange: monaco.Range,
currentSelection: monaco.Range
) => {
console.trace(
'compiler-errors',
`Candidate error range: ${candidateErrorRange.toJSON()}`
);
console.trace(
'compiler-errors',
`Current selection range: ${currentSelection.toJSON()}`
);
if (candidateErrorRange.intersectRanges(currentSelection)) {
console.trace('Intersects.');
return { score: 2 };
}
if (
candidateErrorRange.startLineNumber <=
currentSelection.startLineNumber &&
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
) {
console.trace('Same line.');
return { score: 1 };
}
console.trace('No match');
return undefined;
};
const error = this.errors
.filter((error) => error.uri === uri)
.map((error) => ({
error,
range: ErrorDecoration.rangeOf(error, monacoEditor),
}))
.map(({ error, range }) => {
if (range) {
const priority = calculatePriority(range, monacoSelection);
if (priority) {
return { ...priority, error };
}
}
return undefined;
})
.filter(notEmpty)
.sort((left, right) => right.score - left.score) // highest first
.map(({ error }) => error)
.shift();
if (error) {
this.markAsCurrentError(error);
} else {
console.info(
'compiler-errors',
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
);
}
}
/**
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
* If editor closes, delete the decorators.
*/
private handleEditorDidDispose(uri: string): void {
let i = this.errors.length;
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
while (i--) {
const error = this.errors[i];
if (error.uri === uri) {
this.errors.splice(i, 1);
}
}
this.onDidChangeEmitter.fire(this);
}
/**
* If a document change "destroys" the range of the decoration, the decoration must be removed.
*/
private handleDocumentContentChange(
editor: EditorWidget,
event: TextDocumentChangeEvent
): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
// A decoration location can be "destroyed", hence should be deleted when:
// - deleting range (start != end AND text is empty)
// - inserting text into range (start != end AND text is not empty)
// Filter unrelated delta changes to spare the CPU.
const relevantChanges = event.contentChanges.filter(
({ range: { start, end } }) =>
start.line !== end.line || start.character !== end.character
);
if (!relevantChanges.length) {
return;
}
const resolvedMarkers = this.errors
.filter((error) => error.uri === event.document.uri)
.map((error, index) => {
const range = ErrorDecoration.rangeOf(error, monacoEditor);
if (range) {
return { error, range, index };
}
return undefined;
})
.filter(notEmpty);
const decorationIdsToRemove = relevantChanges
.map(({ range }) => this.p2m.asRange(range))
.map((changeRange) =>
resolvedMarkers.filter(({ range: decorationRange }) =>
changeRange.containsRange(decorationRange)
)
)
.reduce((acc, curr) => acc.concat(curr), [])
.map(({ error, index }) => {
this.errors.splice(index, 1);
return error.id;
});
if (!decorationIdsToRemove.length) {
return;
}
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
this.onDidChangeEmitter.fire(this);
}
private async trackEditors(
errors: Map<string, CoreError.Compiler[]>,
...track: ((editor: EditorWidget) => Disposable)[]
): Promise<Disposable> {
return new DisposableCollection(
...(await Promise.all(
Array.from(errors.keys()).map(async (uri) => {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
return Disposable.NULL;
}
return new DisposableCollection(...track.map((t) => t(editor)));
})
))
);
}
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
const index = this.errors.findIndex((candidate) =>
ErrorDecoration.sameAs(candidate, error)
);
if (index < 0) {
console.warn(
'compiler-errors',
`Failed to mark error ${
error.id
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
({ id }) => id
)}`
);
return;
}
const newError = this.errors[index];
if (
!this.currentError ||
!ErrorDecoration.sameAs(this.currentError, newError)
) {
this.currentError = this.errors[index];
console.log(
'compiler-errors',
`Current error changed to ${this.currentError.id}`
);
this.currentErrorDidChangEmitter.fire(this.currentError);
this.onDidChangeEmitter.fire(this);
}
}
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
private async revealLocationInEditor(
location: Location
): Promise<EditorWidget | undefined> {
const { uri, range } = location;
const editor = await this.editorManager.getByUri(new URI(uri), {
mode: 'activate',
});
if (editor && this.shell) {
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
editor.editor.revealRange(range, { at: this.revealStrategy });
const activeWidget = await this.shell.activateWidget(editor.id);
if (!activeWidget) {
console.warn(
'compiler-errors',
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
);
return editor;
}
if (editor !== activeWidget) {
console.warn(
'compiler-errors',
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
);
}
return editor;
}
console.warn(
'compiler-errors',
`could not found editor widget for URI: ${uri}`
);
return undefined;
}
private groupByResource(
errors: CoreError.Compiler[]
): Map<string, CoreError.Compiler[]> {
return errors.reduce((acc, curr) => {
const {
location: { uri },
} = curr;
let errors = acc.get(uri);
if (!errors) {
errors = [];
acc.set(uri, errors);
}
errors.push(curr);
return acc;
}, new Map<string, CoreError.Compiler[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | EditorWidget
): MaybePromise<MonacoEditor | undefined> {
if (uriOrWidget instanceof EditorWidget) {
const editor = uriOrWidget.editor;
if (editor instanceof MonacoEditor) {
return editor;
}
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);
}
return undefined;
});
}
}
}
export namespace CompilerErrors {
export namespace Commands {
export const NEXT_ERROR: Command = {
id: 'arduino-editor-next-error',
};
export const PREVIOUS_ERROR: Command = {
id: 'arduino-editor-previous-error',
};
}
}

View File

@@ -1,4 +1,9 @@
import { inject, injectable, interfaces } from 'inversify';
import {
inject,
injectable,
interfaces,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Saveable } from '@theia/core/lib/browser/saveable';
@@ -9,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
MenuModelRegistry,
MenuContribution,
@@ -34,14 +39,24 @@ import {
} from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../dialogs/settings/settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { CoreErrorHandler } from './core-error-handler';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
export {
Command,
@@ -84,15 +99,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 +158,7 @@ export abstract class SketchContribution extends Contribution {
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
if (CurrentSketch.isValid(sketch)) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
@@ -139,8 +170,58 @@ export abstract class SketchContribution extends Contribution {
}
}
@injectable()
export class CoreServiceContribution extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(CoreErrorHandler)
protected readonly coreErrorHandler: CoreErrorHandler;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
protected handleError(error: unknown): void {
this.coreErrorHandler.tryHandle(error);
this.tryToastErrorMessage(error);
}
private tryToastErrorMessage(error: unknown): void {
let message: undefined | string = undefined;
if (CoreError.is(error)) {
message = error.message;
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
} else {
try {
message = JSON.stringify(error);
} catch {}
}
if (message) {
const copyAction = nls.localize(
'arduino/coreContribution/copyError',
'Copy error messages'
);
this.messageService.error(message, copyAction).then(async (action) => {
if (action === copyAction) {
const content = await this.outputChannelManager.contentOfChannel(
'Arduino'
);
if (content) {
this.clipboardService.writeText(content);
}
}
});
} else {
throw error;
}
}
}
export namespace Contribution {
export function configure<T>(
export function configure(
bind: interfaces.Bind,
serviceIdentifier: typeof Contribution
): void {

View File

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

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,7 +12,8 @@ import {
SketchContribution,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { MaybePromise, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class Debug extends SketchContribution {
@@ -66,7 +67,7 @@ export class Debug extends SketchContribution {
onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
override onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
@@ -79,55 +80,18 @@ export class Debug extends SketchContribution {
: Debug.Commands.START_DEBUGGING.label
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
this.notificationCenter.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) =>
@@ -136,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
@@ -156,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(

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';
@@ -12,6 +12,8 @@ import {
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@@ -26,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'),
});
@@ -43,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'),
@@ -91,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',
@@ -199,7 +201,7 @@ ${value}
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
@@ -250,10 +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
);
}

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,7 @@ 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()
@@ -43,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)
);
@@ -54,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;
@@ -82,7 +82,7 @@ export abstract class Examples extends SketchContribution {
registerRecursively(
sketchContainerOrPlaceholder:
| SketchContainer
| (Sketch | SketchContainer)[]
| (SketchRef | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
@@ -100,7 +100,7 @@ export abstract class Examples extends SketchContribution {
)
);
} else {
const sketches: Sketch[] = [];
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
@@ -161,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`
}
@@ -201,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);
}

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@@ -14,6 +14,8 @@ import {
} 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 {
@@ -26,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) =>
@@ -84,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',
@@ -117,12 +127,16 @@ export class Help extends Contribution {
order: '6',
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: IDEUpdaterCommands.CHECK_FOR_UPDATES.id,
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',
@@ -167,5 +181,10 @@ export namespace Help {
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';
@@ -16,6 +16,8 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -29,7 +31,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@@ -43,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()
);
@@ -54,7 +55,11 @@ 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,
@@ -77,7 +82,7 @@ export class IncludeLibrary extends SketchContribution {
});
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
@@ -168,7 +173,7 @@ export class IncludeLibrary extends SketchContribution {
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
@@ -14,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(),
});
@@ -25,7 +25,7 @@ export class NewSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New'),
@@ -33,14 +33,14 @@ export class NewSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
@@ -43,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(),
@@ -116,7 +116,7 @@ export class OpenSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
@@ -124,14 +124,14 @@ export class OpenSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,

View File

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

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -11,16 +11,30 @@ import {
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: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
@@ -28,7 +42,7 @@ export class SaveAsSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
@@ -46,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;
}
@@ -90,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 {
@@ -100,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,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -12,10 +12,11 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
@@ -27,7 +28,7 @@ export class SaveSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'),
@@ -35,14 +36,14 @@ export class SaveSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
@@ -53,7 +54,7 @@ export class SaveSketch extends SketchContribution {
async saveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -18,7 +18,7 @@ export class Settings extends SketchContribution {
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
@@ -39,7 +39,7 @@ export class Settings extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id,
label:
@@ -52,7 +52,7 @@ export class Settings extends SketchContribution {
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+,',

View File

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

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -16,15 +15,10 @@ import {
} 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(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@@ -47,8 +41,8 @@ export class UploadSketch extends SketchContribution {
protected readonly menuActionsDisposables = new DisposableCollection();
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields();
@@ -72,7 +66,7 @@ export class UploadSketch extends SketchContribution {
return fqbn + '|' + address;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
@@ -134,7 +128,7 @@ export class UploadSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
@@ -177,7 +171,7 @@ export class UploadSketch extends SketchContribution {
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
@@ -188,7 +182,7 @@ export class UploadSketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
@@ -204,16 +198,17 @@ export class UploadSketch extends SketchContribution {
return;
}
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
@@ -226,8 +221,12 @@ 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;
@@ -246,8 +245,8 @@ export class UploadSketch extends SketchContribution {
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
sketch,
board,
optimizeForDebug,
programmer,
port,
@@ -258,8 +257,8 @@ export class UploadSketch extends SketchContribution {
};
} else {
options = {
sketchUri,
fqbn,
sketch,
board,
optimizeForDebug,
port,
verbose,
@@ -279,18 +278,10 @@ export class UploadSketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = '';
if (typeof e === 'string') {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
}
}
}

View File

@@ -1,12 +1,11 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -14,12 +13,10 @@ import {
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 {
@inject(CoreService)
protected readonly coreService: CoreService;
export class VerifySketch extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -31,7 +28,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,
@@ -50,7 +47,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
@@ -66,7 +63,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
@@ -77,7 +74,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
@@ -95,14 +92,14 @@ export class VerifySketch extends SketchContribution {
// toggle the toolbar button and menu item state.
// verifyInProgress will be set to false whether the compilation fails or not
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
this.verifyInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
@@ -110,12 +107,17 @@ 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,
sketch,
board,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
@@ -127,13 +129,7 @@ export class VerifySketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.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';
@@ -100,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;
}

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

@@ -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

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

View File

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

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
@@ -139,7 +139,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
constructor(
@inject(UploadCertificateDialogProps)
protected readonly props: UploadCertificateDialogProps
protected override readonly props: UploadCertificateDialogProps
) {
super({
title: nls.localize(
@@ -155,7 +155,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -165,21 +165,21 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
override close(): void {
if (this.busy) {
return;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
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 '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { nls } from '@theia/core';
import { IDEUpdaterComponent } from './ide-updater-component';
@@ -70,7 +70,7 @@ export class IDEUpdaterDialogWidget extends ReactWidget {
this.close();
}
close(): void {
override close(): void {
super.close();
this.onClose();
}
@@ -122,7 +122,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
constructor(
@inject(IDEUpdaterDialogProps)
protected readonly props: IDEUpdaterDialogProps
protected override readonly props: IDEUpdaterDialogProps
) {
super({
title: nls.localize(
@@ -138,7 +138,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
return this.widget.updateInfo;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -147,7 +147,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
this.update();
}
async open(
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
@@ -156,17 +156,17 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
}
}
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();
}
close(): void {
override close(): void {
this.widget.dispose();
super.close();
}

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
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';
@@ -9,6 +9,7 @@ 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,
@@ -16,7 +17,10 @@ import {
import { nls } from '@theia/core/lib/common';
import { Settings, SettingsService } from './settings';
import { AdditionalUrlsDialog } from './settings-dialog';
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
import {
AsyncLocalizationProvider,
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
export class SettingsComponent extends React.Component<
SettingsComponent.Props,
@@ -28,35 +32,46 @@ export class SettingsComponent extends React.Component<
super(props);
}
componentDidUpdate(
override componentDidUpdate(
_: SettingsComponent.Props,
prevState: SettingsComponent.State
): void {
if (
this.state &&
prevState &&
JSON.stringify(this.state) !== JSON.stringify(prevState)
JSON.stringify(SettingsComponent.State.toSettings(this.state)) !==
JSON.stringify(SettingsComponent.State.toSettings(prevState))
) {
this.props.settingsService.update(this.state, true);
this.props.settingsService.update(
SettingsComponent.State.toSettings(this.state),
true
);
}
}
componentDidMount(): void {
override componentDidMount(): void {
this.props.settingsService
.settings()
.then((settings) => this.setState(settings));
this.toDispose.push(
.then((settings) =>
this.setState(SettingsComponent.State.fromSettings(settings))
);
this.toDispose.pushAll([
this.props.settingsService.onDidChange((settings) =>
this.setState(settings)
)
);
this.setState((prevState) => ({
...SettingsComponent.State.merge(prevState, settings),
}))
),
this.props.settingsService.onDidReset((settings) =>
this.setState(SettingsComponent.State.fromSettings(settings))
),
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
override render(): React.ReactNode {
if (!this.state) {
return <div />;
}
@@ -201,11 +216,9 @@ export class SettingsComponent extends React.Component<
value={this.state.currentLanguage}
onChange={this.languageDidChange}
>
{this.state.languages.map((label) => (
<option key={label} value={label}>
{label}
</option>
))}
{this.state.languages.map((label) =>
this.toSelectOptions(label)
)}
</select>
<span style={{ marginLeft: '5px' }}>
(
@@ -263,7 +276,7 @@ export class SettingsComponent extends React.Component<
<label className="flex-line">
<input
type="checkbox"
checked={this.state.autoSave === 'on'}
checked={this.state.autoSave !== 'off'}
onChange={this.autoSaveDidChange}
/>
{nls.localize(
@@ -290,8 +303,8 @@ export class SettingsComponent extends React.Component<
<input
className="theia-input stretch with-margin"
type="text"
value={this.state.additionalUrls.join(',')}
onChange={this.additionalUrlsDidChange}
value={this.state.rawAdditionalUrlsValue}
onChange={this.rawAdditionalUrlsValueDidChange}
/>
<i
className="fa fa-window-restore theia-button shrink"
@@ -302,6 +315,24 @@ export class SettingsComponent extends React.Component<
);
}
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">
@@ -475,11 +506,13 @@ export class SettingsComponent extends React.Component<
protected editAdditionalUrlDidClick = async (): Promise<void> => {
const additionalUrls = await new AdditionalUrlsDialog(
this.state.additionalUrls,
AdditionalUrls.parse(this.state.rawAdditionalUrlsValue, ','),
this.props.windowService
).open();
if (additionalUrls) {
this.setState({ additionalUrls });
this.setState({
rawAdditionalUrlsValue: AdditionalUrls.stringify(additionalUrls),
});
}
};
@@ -492,11 +525,11 @@ export class SettingsComponent extends React.Component<
}
};
protected additionalUrlsDidChange = (
protected rawAdditionalUrlsValueDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
additionalUrls: event.target.value.split(',').map((url) => url.trim()),
rawAdditionalUrlsValue: event.target.value,
});
};
@@ -535,7 +568,9 @@ export class SettingsComponent extends React.Component<
protected autoSaveDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ autoSave: event.target.checked ? 'on' : 'off' });
this.setState({
autoSave: event.target.checked ? Settings.AutoSave.DEFAULT_ON : 'off',
});
};
protected quickSuggestionsOtherDidChange = (
@@ -699,5 +734,48 @@ export namespace SettingsComponent {
readonly windowService: WindowService;
readonly localizationProvider: AsyncLocalizationProvider;
}
export type State = Settings & { languages: string[] };
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

@@ -1,7 +1,7 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
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';
@@ -11,6 +11,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
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 {
@@ -55,7 +56,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
constructor(
@inject(SettingsDialogProps)
protected readonly props: SettingsDialogProps
protected override readonly props: SettingsDialogProps
) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
@@ -72,7 +73,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
);
}
protected async isValid(settings: Promise<Settings>): Promise<DialogError> {
protected override async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
@@ -84,7 +85,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
return this.settingsService.settings();
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -96,16 +97,16 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
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);
// calling settingsService.reset() in order to reload the settings from the preferenceService
// and update the UI including changes triggerd from the command palette
// and update the UI including changes triggered from the command palette
this.settingsService.reset();
this.widget.activate();
@@ -168,23 +169,20 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
}
get value(): string[] {
return this.textArea.value
.split('\n')
.map((url) => url.trim())
.filter((url) => !!url);
return AdditionalUrls.parse(this.textArea.value, 'newline');
}
protected onAfterAttach(message: Message): void {
protected override onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected onActivateRequest(message: Message): void {
protected override onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}

View File

@@ -1,4 +1,8 @@
import { injectable, inject, postConstruct } from 'inversify';
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';
@@ -8,19 +12,23 @@ 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 { Index } from '../../../common/types';
import {
AdditionalUrls,
CompilerWarnings,
ConfigService,
FileSystemExt,
Network,
} from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
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 = `${EDITOR_SETTING}.autoSave`;
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`;
@@ -35,13 +43,13 @@ 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 extends Index {
export interface Settings {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: 'on' | 'off'; // `editor.autoSave`
autoSave: Settings.AutoSave; // `files.autoSave`
quickSuggestions: Record<'other' | 'comments' | 'strings', boolean>; // `editor.quickSuggestions`
languages: string[]; // `languages from the plugins`
languages: (string | LanguageInfo)[]; // `languages from the plugins`
currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale`
@@ -53,13 +61,21 @@ export interface Settings extends Index {
sketchbookShowAllFiles: boolean; // `arduino.sketchbook.showAllFiles`
sketchbookPath: string; // CLI
additionalUrls: 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()
@@ -82,8 +98,13 @@ export class SettingsService {
@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;
@@ -120,7 +141,10 @@ export class SettingsService {
'workbench.colorTheme',
'arduino-theme'
),
this.preferenceService.get<'on' | 'off'>(AUTO_SAVE_SETTING, 'on'),
this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING,
Settings.AutoSave.DEFAULT_ON
),
this.preferenceService.get<
Record<'other' | 'comments' | 'strings', boolean>
>(QUICK_SUGGESTIONS_SETTING, {
@@ -167,7 +191,10 @@ export class SettingsService {
async update(settings: Settings, fireDidChange = false): Promise<void> {
await this.ready.promise;
for (const key of Object.keys(settings)) {
this._settings[key] = settings[key];
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);
@@ -176,7 +203,8 @@ export class SettingsService {
async reset(): Promise<void> {
const settings = await this.loadSettings();
return this.update(settings, true);
await this.update(settings, false);
this.onDidResetEmitter.fire(this._settings);
}
async validate(
@@ -252,7 +280,7 @@ export class SettingsService {
await this.savePreference('editor.fontSize', editorFontSize);
await this.savePreference('workbench.colorTheme', themeId);
await this.savePreference('editor.autoSave', autoSave);
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);
@@ -267,13 +295,16 @@ export class SettingsService {
// 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 !== nls.locale && !(currentLanguage === 'en' && nls.locale === undefined)) {
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);
}
window.location.reload();
this.commandService.executeCommand(ElectronCommands.RELOAD.id);
}
return true;

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { BoardUserField } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';

View File

@@ -1,12 +1,12 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
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 '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
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';
@@ -61,7 +61,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
constructor(
@inject(UserFieldsDialogProps)
protected readonly props: UserFieldsDialogProps
protected override readonly props: UserFieldsDialogProps
) {
super({
title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '',
@@ -83,7 +83,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
return this.widget.currentUserFields;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -92,17 +92,17 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected async accept(): Promise<void> {
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) {
@@ -113,7 +113,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
return super.accept();
}
close(): void {
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

@@ -5,7 +5,7 @@ import {
MessageService,
nls,
} from '@theia/core';
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { IDEUpdater, UpdateInfo } from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';

View File

@@ -0,0 +1,13 @@
import * as monaco from '@theia/monaco-editor-core';
/**
* Exclusive "ino" document selector for monaco.
*/
export const InoSelector = selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
function 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,5 +1,5 @@
import { injectable, postConstruct, inject } from 'inversify';
import { Message } from '@phosphor/messaging';
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../theia/dialogs/dialogs';
@@ -38,7 +38,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
}
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
@@ -48,7 +48,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -158,7 +158,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
}
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {
@@ -199,7 +199,7 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
});
}
protected onCloseRequest(message: Message): void {
protected override onCloseRequest(message: Message): void {
super.onCloseRequest(message);
this.accept();
}
@@ -217,7 +217,7 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
return message;
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
this.response = 0;
super.handleEnter(event);
}

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core';
@@ -28,7 +28,7 @@ export class LibraryListWidgetFrontendContribution
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';

View File

@@ -0,0 +1,199 @@
import {
CommandRegistry,
Disposable,
Emitter,
MessageService,
nls,
} from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Board, Port } from '../common/protocol';
import {
Monitor,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
} from '../common/protocol/monitor-service';
import {
PluggableMonitorSettings,
MonitorSettings,
} from '../node/monitor-settings/monitor-settings-provider';
import { BoardsConfig } from './boards/boards-config';
import { BoardsServiceProvider } from './boards/boards-service-provider';
@injectable()
export class MonitorManagerProxyClientImpl
implements MonitorManagerProxyClient
{
// When pluggable monitor messages are received from the backend
// this event is triggered.
// Ideally a frontend component is connected to this event
// to update the UI.
protected readonly onMessagesReceivedEmitter = new Emitter<{
messages: string[];
}>();
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
protected readonly onMonitorSettingsDidChangeEmitter =
new Emitter<MonitorSettings>();
readonly onMonitorSettingsDidChange =
this.onMonitorSettingsDidChangeEmitter.event;
protected readonly onMonitorShouldResetEmitter = new Emitter();
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;
// WebSocket used to handle pluggable monitor communication between
// frontend and backend.
private webSocket?: WebSocket;
private wsPort?: number;
private lastConnectedBoard: BoardsConfig.Config;
private onBoardsConfigChanged: Disposable | undefined;
getWebSocketPort(): number | undefined {
return this.wsPort;
}
constructor(
@inject(MessageService)
protected messageService: MessageService,
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
protected server: MonitorManagerProxyFactory,
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {}
/**
* Connects a localhost WebSocket using the specified port.
* @param addressPort port of the WebSocket
*/
async connect(addressPort: number): Promise<void> {
if (!!this.webSocket) {
if (this.wsPort === addressPort) return;
else this.disconnect();
}
try {
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
} catch {
this.messageService.error(
nls.localize(
'arduino/monitor/unableToConnectToWebSocket',
'Unable to connect to websocket'
)
);
return;
}
this.webSocket.onmessage = (message) => {
const parsedMessage = JSON.parse(message.data);
if (Array.isArray(parsedMessage))
this.onMessagesReceivedEmitter.fire({ messages: parsedMessage });
else if (
parsedMessage.command ===
Monitor.MiddlewareCommand.ON_SETTINGS_DID_CHANGE
) {
this.onMonitorSettingsDidChangeEmitter.fire(parsedMessage.data);
}
};
this.wsPort = addressPort;
}
/**
* Disconnects the WebSocket if connected.
*/
disconnect(): void {
if (!this.webSocket) return;
this.onBoardsConfigChanged?.dispose();
this.onBoardsConfigChanged = undefined;
try {
this.webSocket?.close();
this.webSocket = undefined;
} catch {
this.messageService.error(
nls.localize(
'arduino/monitor/unableToCloseWebSocket',
'Unable to close websocket'
)
);
}
}
async isWSConnected(): Promise<boolean> {
return !!this.webSocket;
}
async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
this.lastConnectedBoard = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
};
if (!this.onBoardsConfigChanged) {
this.onBoardsConfigChanged =
this.boardsServiceProvider.onBoardsConfigChanged(
async ({ selectedBoard, selectedPort }) => {
if (
typeof selectedBoard === 'undefined' ||
typeof selectedPort === 'undefined'
)
return;
// a board is plugged and it's different from the old connected board
if (
selectedBoard?.fqbn !==
this.lastConnectedBoard?.selectedBoard?.fqbn ||
selectedPort?.id !== this.lastConnectedBoard?.selectedPort?.id
) {
this.onMonitorShouldResetEmitter.fire(null);
this.lastConnectedBoard = {
selectedBoard: selectedBoard,
selectedPort: selectedPort,
};
} else {
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
// recreate the listener callback
this.startMonitor();
}
}
);
}
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
await this.server().startMonitor(selectedBoard, selectedPort, settings);
}
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
return this.server().getCurrentSettings(board, port);
}
send(message: string): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.ClientCommand.SEND_MESSAGE,
data: message,
})
);
}
changeSettings(settings: MonitorSettings): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.ClientCommand.CHANGE_SETTINGS,
data: settings,
})
);
}
}

View File

@@ -0,0 +1,278 @@
import { Emitter, Event } from '@theia/core';
import {
FrontendApplicationContribution,
LocalStorageService,
} from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonitorManagerProxyClient } from '../common/protocol';
import { isNullOrUndefined } from '../common/utils';
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _lineEnding: MonitorModel.EOL;
protected _interpolate: boolean;
protected _darkTheme: boolean;
protected _wsPort: number;
protected _serialPort: string;
protected _connected: boolean;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._interpolate = false;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this._darkTheme = false;
this._wsPort = 0;
this._serialPort = '';
this._connected = true;
this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
>();
}
onStart(): void {
this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.then(this.restoreState.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
}
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event;
}
protected restoreState(state: MonitorModel.State): void {
if (!state) {
return;
}
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._lineEnding = state.lineEnding;
this._interpolate = state.interpolate;
this._serialPort = state.serialPort;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
lineEnding: this._lineEnding,
interpolate: this._interpolate,
serialPort: this._serialPort,
});
}
get autoscroll(): boolean {
return this._autoscroll;
}
set autoscroll(autoscroll: boolean) {
if (autoscroll === this._autoscroll) return;
this._autoscroll = autoscroll;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { autoscroll },
});
this.storeState().then(() => {
this.onChangeEmitter.fire({
property: 'autoscroll',
value: this._autoscroll,
});
});
}
toggleAutoscroll(): void {
this.autoscroll = !this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
set timestamp(timestamp: boolean) {
if (timestamp === this._timestamp) return;
this._timestamp = timestamp;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { timestamp },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'timestamp',
value: this._timestamp,
})
);
}
toggleTimestamp(): void {
this.timestamp = !this._timestamp;
}
get lineEnding(): MonitorModel.EOL {
return this._lineEnding;
}
set lineEnding(lineEnding: MonitorModel.EOL) {
if (lineEnding === this._lineEnding) return;
this._lineEnding = lineEnding;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { lineEnding },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'lineEnding',
value: this._lineEnding,
})
);
}
get interpolate(): boolean {
return this._interpolate;
}
set interpolate(interpolate: boolean) {
if (interpolate === this._interpolate) return;
this._interpolate = interpolate;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { interpolate },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'interpolate',
value: this._interpolate,
})
);
}
get darkTheme(): boolean {
return this._darkTheme;
}
set darkTheme(darkTheme: boolean) {
if (darkTheme === this._darkTheme) return;
this._darkTheme = darkTheme;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { darkTheme },
});
this.onChangeEmitter.fire({
property: 'darkTheme',
value: this._darkTheme,
});
}
get wsPort(): number {
return this._wsPort;
}
set wsPort(wsPort: number) {
if (wsPort === this._wsPort) return;
this._wsPort = wsPort;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { wsPort },
});
this.onChangeEmitter.fire({
property: 'wsPort',
value: this._wsPort,
});
}
get serialPort(): string {
return this._serialPort;
}
set serialPort(serialPort: string) {
if (serialPort === this._serialPort) return;
this._serialPort = serialPort;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { serialPort },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'serialPort',
value: this._serialPort,
})
);
}
get connected(): boolean {
return this._connected;
}
set connected(connected: boolean) {
if (connected === this._connected) return;
this._connected = connected;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { connected },
});
this.onChangeEmitter.fire({
property: 'connected',
value: this._connected,
});
}
protected onMonitorSettingsDidChange = (settings: MonitorSettings): void => {
const { monitorUISettings } = settings;
if (!monitorUISettings) return;
const {
autoscroll,
interpolate,
lineEnding,
timestamp,
darkTheme,
wsPort,
serialPort,
connected,
} = monitorUISettings;
if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
if (!isNullOrUndefined(interpolate)) this.interpolate = interpolate;
if (!isNullOrUndefined(lineEnding)) this.lineEnding = lineEnding;
if (!isNullOrUndefined(timestamp)) this.timestamp = timestamp;
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
if (!isNullOrUndefined(connected)) this.connected = connected;
};
}
// TODO: Move this to /common
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
lineEnding: EOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connected: boolean;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}
export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}

View File

@@ -1,4 +1,8 @@
import { inject, injectable, postConstruct } from 'inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
@@ -14,6 +18,10 @@ import {
Config,
Sketch,
} from '../common/protocol';
import {
FrontendApplicationStateService,
FrontendApplicationState,
} from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class NotificationCenter
@@ -22,8 +30,11 @@ export class NotificationCenter
@inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly indexUpdatedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<string>();
protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{
config: Config | undefined;
@@ -45,6 +56,8 @@ export class NotificationCenter
protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[];
}>();
private readonly onAppStateDidChangeEmitter =
new Emitter<FrontendApplicationState>();
protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter,
@@ -68,10 +81,16 @@ export class NotificationCenter
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
readonly onAppStateDidChange = this.onAppStateDidChangeEmitter.event;
@postConstruct()
protected init(): void {
this.server.setClient(this);
this.toDispose.push(
this.appStateService.onStateChanged((state) =>
this.onAppStateDidChangeEmitter.fire(state)
)
);
}
onStop(): void {
@@ -82,8 +101,8 @@ export class NotificationCenter
this.indexUpdatedEmitter.fire();
}
notifyDaemonStarted(): void {
this.daemonStartedEmitter.fire();
notifyDaemonStarted(port: string): void {
this.daemonStartedEmitter.fire(port);
}
notifyDaemonStopped(): void {

View File

@@ -1,7 +1,9 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
OutputChannelManager,
OutputChannelSeverity,
} from '@theia/output/lib/browser/output-channel';
import {
OutputMessage,
ProgressMessage,
@@ -10,13 +12,10 @@ import {
@injectable()
export class ResponseServiceImpl implements ResponseServiceArduino {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
private readonly outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
private readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
@@ -25,13 +24,22 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
}
appendToOutput(message: OutputMessage): void {
const { chunk } = message;
const { chunk, severity } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true });
channel.append(chunk);
channel.append(chunk, mapSeverity(severity));
}
reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress);
}
}
function mapSeverity(severity?: OutputMessage.Severity): OutputChannelSeverity {
if (severity === OutputMessage.Severity.Error) {
return OutputChannelSeverity.Error;
} else if (severity === OutputMessage.Severity.Warning) {
return OutputChannelSeverity.Warning;
}
return OutputChannelSeverity.Info;
}

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 { AbstractViewContribution, codicon } from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
@@ -8,9 +8,10 @@ import {
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MonitorModel } from '../../monitor-model';
import { MonitorManagerProxyClient } from '../../../common/protocol';
export namespace SerialMonitor {
export namespace Commands {
@@ -47,10 +48,15 @@ export class MonitorViewContribution
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
MonitorWidget.ID + ':toggle-toolbar';
static readonly RESET_SERIAL_MONITOR = MonitorWidget.ID + ':reset';
@inject(SerialModel) protected readonly model: SerialModel;
constructor(
@inject(MonitorModel)
protected readonly model: MonitorModel,
constructor() {
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient
) {
super({
widgetId: MonitorWidget.ID,
widgetName: MonitorWidget.LABEL,
@@ -60,9 +66,10 @@ export class MonitorViewContribution
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'CtrlCmd+Shift+M',
});
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
@@ -95,7 +102,7 @@ export class MonitorViewContribution
});
}
registerCommands(commands: CommandRegistry): void {
override registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
@@ -118,6 +125,10 @@ export class MonitorViewContribution
}
);
}
commands.registerCommand(
{ id: MonitorViewContribution.RESET_SERIAL_MONITOR },
{ execute: () => this.reset() }
);
}
protected async toggle(): Promise<void> {
@@ -129,6 +140,14 @@ export class MonitorViewContribution
}
}
protected async reset(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
await this.openView({ activate: true, reveal: true });
}
}
protected renderAutoScrollButton(): React.ReactNode {
return (
<React.Fragment key="autoscroll-toolbar-item">

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { postConstruct, injectable, inject } from 'inversify';
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { OptionsType } from 'react-select/src/types';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
@@ -9,14 +9,14 @@ import {
Widget,
MessageLoop,
} from '@theia/core/lib/browser/widgets';
import { SerialConfig } from '../../../common/protocol/serial-service';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SerialModel } from '../serial-model';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialMonitorSendInput } from './serial-monitor-send-input';
import { SerialMonitorOutput } from './serial-monitor-send-output';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { nls } from '@theia/core/lib/common';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorWidget extends ReactWidget {
@@ -26,14 +26,7 @@ export class MonitorWidget extends ReactWidget {
);
static readonly ID = 'serial-monitor';
@inject(SerialModel)
protected readonly serialModel: SerialModel;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected settings: MonitorSettings = {};
protected widgetHeight: number;
@@ -48,7 +41,16 @@ export class MonitorWidget extends ReactWidget {
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
constructor(
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel,
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {
super();
this.id = MonitorWidget.ID;
this.title.label = MonitorWidget.LABEL;
@@ -57,17 +59,30 @@ export class MonitorWidget extends ReactWidget {
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => this.serialConnection.closeWStoBE())
Disposable.create(() => this.monitorManagerProxy.disconnect())
);
}
@postConstruct()
protected init(): void {
protected override onBeforeAttach(msg: Message): void {
this.update();
this.toDispose.push(
this.serialConnection.onConnectionChanged(() => this.clearConsole())
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
this.getCurrentSettings().then(this.onMonitorSettingsDidChange.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
this.toDispose.push(this.serialModel.onChange(() => this.update()));
this.monitorManagerProxy.startMonitor();
}
onMonitorSettingsDidChange(settings: MonitorSettings): void {
this.settings = {
...this.settings,
pluggableMonitorSettings: {
...this.settings.pluggableMonitorSettings,
...settings.pluggableMonitorSettings,
},
};
this.update();
}
clearConsole(): void {
@@ -75,21 +90,16 @@ export class MonitorWidget extends ReactWidget {
this.update();
}
dispose(): void {
override dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.serialConnection.openWSToBE();
}
onCloseRequest(msg: Message): void {
protected override onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
@@ -97,13 +107,13 @@ export class MonitorWidget extends ReactWidget {
}
}
protected onResize(msg: Widget.ResizeMessage): void {
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
@@ -119,7 +129,7 @@ export class MonitorWidget extends ReactWidget {
};
protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<SerialModel.EOL>
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
return [
{
@@ -144,32 +154,40 @@ export class MonitorWidget extends ReactWidget {
];
}
protected get baudRates(): OptionsType<
SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
> {
const baudRates: Array<SerialConfig.BaudRate> = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
private getCurrentSettings(): Promise<MonitorSettings> {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
if (!board || !port) {
return Promise.resolve(this.settings || {});
}
return this.monitorManagerProxy.getCurrentSettings(board, port);
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const baudrate = this.settings?.pluggableMonitorSettings
? this.settings.pluggableMonitorSettings.baudrate
: undefined;
const baudrateOptions = baudrate?.values.map((b) => ({
label: b + ' baud',
value: b,
}));
const baudrateSelectedOption = baudrateOptions?.find(
(b) => b.value === baudrate?.selectedValue
);
const lineEnding =
lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find((item) => item.value === this.serialModel.baudRate) ||
baudRates[4]; // Defaults to `9600`.
this.lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || this.lineEndings[1]; // Defaults to `\n`.
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
serialConnection={this.serialConnection}
boardsServiceProvider={this.boardsServiceProvider}
monitorModel={this.monitorModel}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
@@ -178,26 +196,28 @@ export class MonitorWidget extends ReactWidget {
<div className="select">
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
options={this.lineEndings}
value={lineEnding}
onChange={this.onChangeLineEnding}
/>
</div>
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
value={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
{baudrateOptions && baudrateSelectedOption && (
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudrateOptions}
value={baudrateSelectedOption}
onChange={this.onChangeBaudRate}
/>
</div>
)}
</div>
</div>
<div className="body">
<SerialMonitorOutput
serialModel={this.serialModel}
serialConnection={this.serialConnection}
monitorModel={this.monitorModel}
monitorManagerProxy={this.monitorManagerProxy}
clearConsoleEvent={this.clearOutputEmitter.event}
height={Math.floor(this.widgetHeight - 50)}
/>
@@ -208,18 +228,26 @@ export class MonitorWidget extends ReactWidget {
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.serialConnection.send(value);
this.monitorManagerProxy.send(value);
}
protected readonly onChangeLineEnding = (
option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
) => {
this.serialModel.lineEnding = option.value;
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
): void => {
this.monitorModel.lineEnding = option.value;
};
protected readonly onChangeBaudRate = (
option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
) => {
this.serialModel.baudRate = option.value;
protected readonly onChangeBaudRate = ({
value,
}: {
value: string;
}): void => {
this.getCurrentSettings().then(({ pluggableMonitorSettings }) => {
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate'])
return;
const baudRateSettings = pluggableMonitorSettings['baudrate'];
baudRateSettings.selectedValue = value;
this.monitorManagerProxy.changeSettings({ pluggableMonitorSettings });
});
};
}

View File

@@ -1,14 +1,15 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board } from '../../../common/protocol/boards-service';
import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from '../plotter/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
export namespace SerialMonitorSendInput {
export interface Props {
readonly serialConnection: SerialConnectionManager;
readonly boardsServiceProvider: BoardsServiceProvider;
readonly monitorModel: MonitorModel;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
@@ -26,41 +27,33 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: false };
this.state = { text: '', connected: true };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
componentDidMount(): void {
this.props.serialConnection.isBESerialConnected().then((connected) => {
this.setState({ connected });
});
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
if (
messages.command ===
SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED &&
'connected' in messages.data
) {
this.setState({ connected: messages.data.connected });
}
}),
]);
override componentDidMount(): void {
this.setState({ connected: this.props.monitorModel.connected });
this.toDisposeBeforeUnmount.push(
this.props.monitorModel.onChange(({ property }) => {
if (property === 'connected')
this.setState({ connected: this.props.monitorModel.connected });
})
);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.state.connected ? '' : 'warning'}`}
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
@@ -69,15 +62,22 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected shouldShowWarning(): boolean {
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return !this.state.connected || !board || !port;
}
protected get placeholder(): string {
const serialConfig = this.props.serialConnection.getConfig();
if (!this.state.connected || !serialConfig) {
if (this.shouldShowWarning()) {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
);
}
const { board, port } = serialConfig;
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return nls.localize(
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}')",
@@ -102,7 +102,7 @@ export class SerialMonitorSendInput extends React.Component<
}
protected onSend(): void {
this.props.onSend(this.state.text);
this.props.onSend(this.state.text + this.props.monitorModel.lineEnding);
this.setState({ text: '' });
}

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { areEqual, FixedSizeList as List } from 'react-window';
import { SerialModel } from '../serial-model';
import { SerialConnectionManager } from '../serial-connection-manager';
import dateFormat = require('dateformat');
import { messagesToLines, truncateLines } from './monitor-utils';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
export type Line = { message: string; timestamp?: Date; lineLen: number };
@@ -24,12 +24,12 @@ export class SerialMonitorOutput extends React.Component<
this.listRef = React.createRef();
this.state = {
lines: [],
timestamp: this.props.serialModel.timestamp,
timestamp: this.props.monitorModel.timestamp,
charCount: 0,
};
}
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<List
className="serial-monitor-messages"
@@ -51,21 +51,20 @@ export class SerialMonitorOutput extends React.Component<
);
}
shouldComponentUpdate(): boolean {
override shouldComponentUpdate(): boolean {
return true;
}
componentDidMount(): void {
override componentDidMount(): void {
this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => {
const [newLines, totalCharCount] = messagesToLines(
messages,
this.state.lines,
this.state.charCount
);
const [lines, charCount] = truncateLines(newLines, totalCharCount);
this.setState({
lines,
charCount,
@@ -75,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
this.props.clearConsoleEvent(() =>
this.setState({ lines: [], charCount: 0 })
),
this.props.serialModel.onChange(({ property }) => {
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.serialModel;
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
if (property === 'autoscroll') {
@@ -87,13 +86,13 @@ export class SerialMonitorOutput extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
scrollToBottom = ((): void => {
if (this.listRef.current && this.props.serialModel.autoscroll) {
if (this.listRef.current && this.props.monitorModel.autoscroll) {
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
}
}).bind(this);
@@ -115,8 +114,10 @@ const _Row = ({
return (
(data.lines[index].lineLen && (
<div style={style}>
{timestamp}
{data.lines[index].message}
<pre>
{timestamp}
{data.lines[index].message}
</pre>
</div>
)) ||
null
@@ -126,8 +127,8 @@ const Row = React.memo(_Row, areEqual);
export namespace SerialMonitorOutput {
export interface Props {
readonly serialModel: SerialModel;
readonly serialConnection: SerialConnectionManager;
readonly monitorModel: MonitorModel;
readonly monitorManagerProxy: MonitorManagerProxyClient;
readonly clearConsoleEvent: Event<void>;
readonly height: number;
}

View File

@@ -1,20 +1,19 @@
import { ThemeService } from '@theia/core/lib/browser/theming';
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
Command,
CommandRegistry,
MaybePromise,
MenuModelRegistry,
} from '@theia/core';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { Contribution } from '../../contributions/contribution';
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
import { ipcRenderer } from '@theia/electron/shared/electron';
import { SerialConfig } from '../../../common/protocol';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from './protocol';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
const queryString = require('query-string');
export namespace SerialPlotterContribution {
@@ -24,6 +23,11 @@ export namespace SerialPlotterContribution {
label: 'Serial Plotter',
category: 'Arduino',
};
export const RESET: Command = {
id: 'serial-plotter-reset',
label: 'Reset Serial Plotter',
category: 'Arduino',
};
}
}
@@ -33,19 +37,19 @@ export class PlotterFrontendContribution extends Contribution {
protected url: string;
protected wsPort: number;
@inject(SerialModel)
protected readonly model: SerialModel;
@inject(MonitorModel)
protected readonly model: MonitorModel;
@inject(ThemeService)
protected readonly themeService: ThemeService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
onStart(app: FrontendApplication): MaybePromise<void> {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
@@ -53,16 +57,21 @@ export class PlotterFrontendContribution extends Contribution {
this.window = null;
}
});
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
return super.onStart(app);
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
execute: this.connect.bind(this),
execute: this.startPlotter.bind(this),
});
registry.registerCommand(SerialPlotterContribution.Commands.RESET, {
execute: () => this.reset(),
});
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: SerialPlotterContribution.Commands.OPEN.id,
label: SerialPlotterContribution.Commands.OPEN.label,
@@ -70,12 +79,13 @@ export class PlotterFrontendContribution extends Contribution {
});
}
async connect(): Promise<void> {
async startPlotter(): Promise<void> {
await this.monitorManagerProxy.startMonitor();
if (!!this.window) {
this.window.focus();
return;
}
const wsPort = this.serialConnection.getWsPort();
const wsPort = this.monitorManagerProxy.getWebSocketPort();
if (wsPort) {
this.open(wsPort);
} else {
@@ -84,15 +94,10 @@ export class PlotterFrontendContribution extends Contribution {
}
protected async open(wsPort: number): Promise<void> {
const initConfig: Partial<SerialPlotter.Config> = {
baudrates: SerialConfig.BaudRates.map((b) => b),
currentBaudrate: this.model.baudRate,
currentLineEnding: this.model.lineEnding,
const initConfig = {
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
wsPort,
interpolate: this.model.interpolate,
connected: await this.serialConnection.isBESerialConnected(),
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
serialPort: this.model.serialPort,
};
const urlWithParams = queryString.stringifyUrl(
{
@@ -103,4 +108,11 @@ export class PlotterFrontendContribution extends Contribution {
);
this.window = window.open(urlWithParams, 'serialPlotter');
}
protected async reset(): Promise<void> {
if (!!this.window) {
this.window.close();
await this.startPlotter();
}
}
}

View File

@@ -1,26 +0,0 @@
export namespace SerialPlotter {
export type Config = {
currentBaudrate: number;
baudrates: number[];
currentLineEnding: string;
darkTheme: boolean;
wsPort: number;
interpolate: boolean;
serialPort: string;
connected: boolean;
generate?: boolean;
};
export namespace Protocol {
export enum Command {
PLOTTER_SET_BAUDRATE = 'PLOTTER_SET_BAUDRATE',
PLOTTER_SET_LINE_ENDING = 'PLOTTER_SET_LINE_ENDING',
PLOTTER_SET_INTERPOLATE = 'PLOTTER_SET_INTERPOLATE',
PLOTTER_SEND_MESSAGE = 'PLOTTER_SEND_MESSAGE',
MIDDLEWARE_CONFIG_CHANGED = 'MIDDLEWARE_CONFIG_CHANGED',
}
export type Message = {
command: SerialPlotter.Protocol.Command;
data?: any;
};
}
}

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