Compare commits

..

54 Commits

Author SHA1 Message Date
Akos Kitta
f34f594653 Use 0.16.0 CLI. Bumped version to 2.0.0-beta.2
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-15 14:37:27 +01:00
Akos Kitta
1dc7a89dd9 ATL-935: Better support for opening large projects
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
1280a344a7 ATL-806: Fixed always_export_binaries CLI config
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
f1c80041fe GH-432: Made compile/verify work on dirty editors
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
ec1abcc989 Fixed the build status badge in the main README.
Closes arduino/arduino-ide#7

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
1c03d12165 GH-423: Do not copy copyright from about dialog
Closes arduino/arduino-pro-ide#423

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
9180f4e378 Removed the 'Beta' status from title and about.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
516c79276c GH-430: Fixed 'Close' confirmation is ignored.
Updated to lates Theia: `1.11.0-next.c9db9754`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
Akos Kitta
e639d7da06 GH-422: Changed the default verbose mode.
From now on, compile/upload is not verbose.

Closes arduino/arduino-pro-ide#422.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-12 13:06:11 +01:00
per1234
754097877b Make the example values in the bug report template more relevant
The bug report template provides some example values for OS and version. The previous examples were completely
irrelevant to this project due to using an unsupported OS and a different versioning scheme. This might cause confusion,
or at least seem odd, to the contributor.
2021-02-11 23:21:57 -08:00
per1234
b847cff615 Add issue templates
At the first step of creating an issue, a menu of issue types will be presented:

- Bug report
- Feature request
- Report a security vulnerability

If one of the first two are selected, the issue body field will be pre-filled with the template Markdown.

"Report a security vulnerability" will take them to Arduino's global security disclosure policy, which provides further guidance.

If none of the categories in the issue type chooser are applicable, the "Open a blank issue." link at the bottom of the
page can be selected, which will provide the non-templated issue creation experience.

These templates are copies of https://github.com/arduino/arduino-pro-ide/tree/master/.github/ISSUE_TEMPLATE
2021-02-11 23:21:57 -08:00
Akos Kitta
b3deb2fd34 Aligned workflow/docs with the default branch name
This is required after switching the default from `master` to `main`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 15:12:23 +01:00
Sebastian Romero
b2641f56be Remove underscore from product name 2021-02-11 11:03:36 +01:00
Akos Kitta
ba8885c8c8 ATL-938: Added menu group categories.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
3e92567d52 GH-421: Cleaned up the _Output_ channel UI.
- Merged the Arduino channels into one,
 - Removed the channel selector dropdown from the UI.

Closes arduino/arduino-pro-ide#421.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
19613de1b4 ATL-936: Fixed the theme dropdown in the settings.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
01ef138d9a ATL-551: Removed the _Advanced Mode_ toggle.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
39b8a602c7 [UX]: Fixed button styles with the HC theme.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
57c50fefe3 ATL-885: Refined the 'Close' behavior.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
23877f162c ATL-879: Avoid reopening the same sketch.
Instead of reopening it, focus the existing window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
96f0722d56 Removed the arduino-debugger-extension extension
We use the `cortex-debug` VSX.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Sebastian Romero
48c6c53b9b Add rebranded icons 2021-02-11 09:59:46 +01:00
Akos Kitta
b8647f16ad Renamed the application. Updated links and version
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
cfe9e8ec95 Bumped version to 0.1.4. Use CLI 0.15.2.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-11 09:59:46 +01:00
Akos Kitta
291179489f Reenabled the nightly build.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 18:33:44 +01:00
Akos Kitta
71cfa06fc2 ATL-878: Fixed boards dropdown with the HC theme.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
68b1f8d4f2 Implemented the Network tab.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
e957ac4331 ATL-74: Added Export compiled Binary.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
b65867d2f4 ATL-58: Archive sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
a8e60698a8 ATL-836: Implemented 'Add File...'.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
52b0fd35a3 ATL-93: Added Support for .pde sketch file format.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
b1ab6df8b7 Reimplemented sketchbook watcher.
Moved it to the frontend.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
911875665d Do not bail when wiping the temp sketch has failed
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
113fe38850 Fixed the Views menu registration.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
770e0b592a Log sketchbook watch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
79bf0a123f Fixed the C++ extension download link.
It was a 404 due to some changes in Open VSX.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
8eaf03a299 Fixed the app packager on Windows.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
f36d261dcd [debug]: No await for the watcher in sketchbook.
This seems to block the workspace init on Windows in bundled electron.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
a99093624f Updated to 0.15.0-rc1 CLI and 12.x snapshot clangd.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
1f544b2656 ATL-546: Added UI for settings.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-02-03 17:44:36 +01:00
Akos Kitta
1742c53015 ATL-812: Enhanced the Help menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
e33af0d78a Use init instead of dump for config fallback.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
24dfffa976 ATL-835: Support for JSON file type.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
6626701bc9 ATL-815: Implemented Open Recent.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
66b711f43c Made the CLI schema validation bit more relaxed.
Both `metrics` and `telemetry` are generated by `config dump`.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
c6b125011e ATL-814: Show boards and ports under Tools menu.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
f6b5dd24e2 Patched the Theia menu factory.
eclipse-theia/theia#8977

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
14919bba1b Fixed whitespace issue in About dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
0b89cc4a3b Updated to the HEAD CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
284181b874 ATL-811: Removed Run menu item from the app menu
It came from the `@theia/debug` extension.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-26 14:07:07 +01:00
Akos Kitta
db2967084f Added the Sketchbook menu with FS event tracking
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2021-01-18 13:22:38 +01:00
Akos Kitta
1b6d9eccdc Disabled the CRON (nightly build) job.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-17 17:32:51 +01:00
Akos Kitta
258b1e903e GH-393: Do not use clangd from the $PATH.
Closes: arduino/arduino-pro-ide#393

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-15 12:43:11 +01:00
Akos Kitta
00a3ee34c8 Added a script to update the versions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2020-12-14 15:33:06 +01:00
137 changed files with 4983 additions and 3753 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

View File

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

View File

@@ -1,15 +1,15 @@
name: Arduino Pro IDE
name: Arduino IDE
on:
push:
branches:
- master
- main
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
pull_request:
branches:
- master
- main
schedule:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
@@ -22,7 +22,6 @@ jobs:
- os: windows-latest
- os: ubuntu-latest
- os: macos-latest
# - os: rsora-rpi-arm # self-hosted armhf
runs-on: ${{ matrix.config.os }}
timeout-minutes: 90
@@ -44,13 +43,12 @@ jobs:
- name: Package
shell: bash
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
# See: https://www.electron.build/code-signing
@@ -99,7 +97,7 @@ jobs:
if [ "$IS_RELEASE" = true ]; then
export BODY=$(echo -e "$GIT_LOG")
else
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-pro-ide/releases/tag/$LATEST_TAG)")
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
if [ -z "$GIT_LOG" ]; then
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
else
@@ -114,7 +112,7 @@ jobs:
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master')
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v2
with:
name: build-artifacts
@@ -122,7 +120,7 @@ jobs:
publish:
needs: changelog
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master')
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
@@ -136,7 +134,7 @@ jobs:
env:
PLUGIN_SOURCE: "build-artifacts/*"
PLUGIN_STRIP_PREFIX: "build-artifacts/"
PLUGIN_TARGET: "/arduino-pro-ide/nightly"
PLUGIN_TARGET: "/arduino-ide/nightly"
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -160,8 +158,7 @@ jobs:
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.2.0
with:
repo_token: ${{ secrets.RELEASE_TOKEN }}
repo_name: arduino/arduino-pro-ide
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
file: build-artifacts/*
tag: ${{ github.ref }}
@@ -173,7 +170,7 @@ jobs:
env:
PLUGIN_SOURCE: "build-artifacts/*"
PLUGIN_STRIP_PREFIX: "build-artifacts/"
PLUGIN_TARGET: "/arduino-pro-ide"
PLUGIN_TARGET: "/arduino-ide"
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -1,24 +0,0 @@
image:
file: Dockerfile
ports:
- port: 3000
onOpen: open-preview
- port: 5900
onOpen: ignore
- port: 6080
onOpen: ignore
tasks:
- init: >
yarn &&
yarn --cwd ./browser-app start
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addComment: false
addBadge: false

22
.vscode/tasks.json vendored
View File

@@ -4,7 +4,7 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino Pro IDE - Rebuild Electron App",
"label": "Arduino IDE - Rebuild Electron App",
"type": "shell",
"command": "yarn rebuild:browser && yarn rebuild:electron",
"group": "build",
@@ -15,7 +15,7 @@
}
},
{
"label": "Arduino Pro IDE - Start Browser App",
"label": "Arduino IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
@@ -26,7 +26,7 @@
}
},
{
"label": "Arduino Pro IDE - Watch IDE Extension",
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -37,7 +37,7 @@
}
}
{
"label": "Arduino Pro IDE - Watch Browser App",
"label": "Arduino IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
@@ -48,7 +48,7 @@
}
},
{
"label": "Arduino Pro IDE - Watch Electron App",
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
"command": "yarn --cwd ./electron-app watch",
"group": "build",
@@ -59,19 +59,19 @@
}
},
{
"label": "Arduino Pro IDE - Watch All [Browser]",
"label": "Arduino IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Browser App"
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Browser App"
]
},
{
"label": "Arduino Pro IDE - Watch All [Electron]",
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",
"dependsOn": [
"Arduino Pro IDE - Watch IDE Extension",
"Arduino Pro IDE - Watch Electron App"
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Electron App"
]
}
]

View File

@@ -1,10 +1,10 @@
# Arduino Pro IDE
# Arduino IDE
[![Arduino Pro IDE](https://github.com/bcmi-labs/arduino-editor/workflows/Arduino%20Pro%20IDE/badge.svg)](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22)
[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
### Download
You can download the latest version of the Arduino Pro IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-pro-ide/releases) or following the links in the following table.
You can download the latest version of the Arduino IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-ide/releases) or following the links in the following table.
#### Latest version
@@ -15,20 +15,20 @@ Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...]
Windows | | [Windows 64 bit installer]<br />[Windows 64 bit MSI]<br />[Windows 64 bit ZIP] |
macOS | | [macOS 64 bit] |
[🚧 Work in progress...]: https://github.com/arduino/arduino-pro-ide/issues/287
[Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Linux_64bit.zip
[Windows 64 bit installer]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Windows_64bit.exe
[Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Windows_64bit.msi
[Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_Windows_64bit.zip
[macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/arduino-pro-ide_latest_macOS_64bit.dmg
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
[Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Linux_64bit.zip
[Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.exe
[Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.msi
[Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.zip
[macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_macOS_64bit.dmg
#### Previous versions
These are available from the [GitHub releases page](https://github.com/arduino/arduino-pro-ide/releases).
These are available from the [GitHub releases page](https://github.com/arduino/arduino-ide/releases).
#### Nightly builds
These builds are generated every day at 03:00 GMT from the `master` branch and
These builds are generated every day at 03:00 GMT from the `main` branch and
should be considered unstable. In order to get the latest nightly build
available for the supported platform, use the following links:
@@ -39,12 +39,12 @@ 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-pro-ide/issues/287
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Windows_64bit.exe
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Windows_64bit.msi
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-pro-ide/nightly/arduino-pro-ide_nightly-latest_macOS_64bit.dmg
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, redirecting to latest
generated builds by replacing `latest` with the latest available build
@@ -54,7 +54,7 @@ macOS | | [Nightly macOS 64 bit]
### Build from source
If youre familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
project, you should be able to build the Arduino Pro IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
### Build
```sh
@@ -75,8 +75,8 @@ yarn start
This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22).
- _Snapshot_ builds run when changes are pushed to the `master` branch, or when a PR is created against the `master` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
- _Nightly_ builds run every day at 03:00 GMT from the `master` branch.
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
- Create a local tag:
```sh
@@ -87,6 +87,38 @@ This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-e
git push origin 1.2.3
```
### Creating a GH release
This section guides you through how to create a new release. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
- Make sure the `main` state represents what you want to release and you're on `main`.
- Prepare a release-candidate build on a branch:
```bash
git branch 0.2.0-rc \
&& git checkout 0.2.0-rc
```
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
```bash
yarn update:version 0.2.0
```
- This should generate multiple outgoing changes with the version update.
- Commit your changes and push to the remote:
```bash
git add . \
&& git commit -s -m "Updated versions to 0.2.0" \
&& git push
```
- Create the GH PR the workflow starts automatically.
- Once you're happy with the RC, merge the changes to the `main`.
- Create a tag and push it:
```bash
git tag -a 0.2.0 -m "0.2.0" \
&& git push origin 0.2.0
```
- The release build starts automatically and uploads the artifacts with the changelog to the Pro IDE [release page](https://github.com/arduino/arduino-ide/releases).
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
- If you do not like the generated changelog, modify it and update the GH release.
### FAQ
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
@@ -94,9 +126,9 @@ This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-e
- Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?
- A: [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
- Windows: `C:\path\to\Arduino Pro IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino Pro IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino Pro IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
### Architecture overview

View File

@@ -1,30 +0,0 @@
{
"name": "arduino-debugger-extension",
"version": "0.1.3",
"description": "An extension for debugging Arduino programs",
"license": "MIT",
"dependencies": {
"@theia/debug": "next",
"arduino-ide-extension": "0.1.3",
"cdt-gdb-adapter": "^0.0.14",
"vscode-debugadapter": "^1.26.0",
"vscode-debugprotocol": "^1.26.0"
},
"scripts": {
"prepare": "yarn run clean && yarn run build",
"clean": "rimraf lib",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && yarn lint",
"watch": "tsc -w"
},
"files": [
"lib",
"src"
],
"theiaExtensions": [
{
"backend": "lib/node/arduino-debug-backend-module",
"frontend": "lib/browser/arduino-debug-frontend-module"
}
]
}

View File

@@ -1,39 +0,0 @@
import { DebugConfigurationManager } from "@theia/debug/lib/browser/debug-configuration-manager";
import { injectable } from "inversify";
@injectable()
export class ArduinoDebugConfigurationManager extends DebugConfigurationManager {
get defaultDebugger(): Promise<string | undefined> {
return this.debug.getDebuggersForLanguage('ino').then(debuggers => {
if (debuggers.length === 0)
return undefined;
return debuggers[0].type;
});
}
protected async selectDebugType(): Promise<string | undefined> {
const widget = this.editorManager.currentEditor;
if (!widget) {
return this.defaultDebugger;
}
const { languageId } = widget.editor.document;
const debuggers = await this.debug.getDebuggersForLanguage(languageId);
if (debuggers.length === 0) {
return this.defaultDebugger;
}
return this.quickPick.show(debuggers.map(
({ label, type }) => ({ label, value: type }),
{ placeholder: 'Select Environment' })
);
}
async createDefaultConfiguration(): Promise<void> {
const { model } = this;
if (model) {
await this.doCreate(model);
await this.updateModels();
}
}
}

View File

@@ -1,133 +0,0 @@
import { injectable, inject } from 'inversify';
import { MenuModelRegistry, MessageService, Command, CommandRegistry } from '@theia/core';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { DebugFrontendApplicationContribution, DebugCommands } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FileSystem } from '@theia/filesystem/lib/common';
import URI from '@theia/core/lib/common/uri';
import { EditorManager } from '@theia/editor/lib/browser';
import { EditorMode } from "arduino-ide-extension/lib/browser/editor-mode";
import { SketchesService } from 'arduino-ide-extension/lib/common/protocol/sketches-service';
import { ArduinoToolbar } from 'arduino-ide-extension/lib/browser/toolbar/arduino-toolbar';
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
export namespace ArduinoDebugCommands {
export const START_DEBUG: Command = {
id: 'arduino-start-debug',
label: 'Start Debugging'
}
}
@injectable()
export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendApplicationContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(MessageService)
protected readonly messageService: MessageService;
async start(noDebug?: boolean, debugSessionOptions?: DebugSessionOptions): Promise<void> {
const configurations = this.configurations as ArduinoDebugConfigurationManager;
let current = debugSessionOptions ? debugSessionOptions : configurations.current;
// If no configurations are currently present, create them
if (!current) {
await configurations.createDefaultConfiguration();
current = configurations.current;
}
if (current) {
if (noDebug !== undefined) {
current = {
...current,
configuration: {
...current.configuration,
noDebug
}
};
}
if (current.configuration.type === 'arduino') {
const wsStat = this.workspaceService.workspace;
let sketchFileURI: URI | undefined;
if (wsStat && await this.sketchesService.isSketchFolder(wsStat.resource.toString())) {
const wsPath = wsStat.resource.path;
const sketchFilePath = wsPath.join(wsPath.name + '.ino').toString();
sketchFileURI = new URI(sketchFilePath);
} else if (this.editorManager.currentEditor) {
const editorURI = this.editorManager.currentEditor.getResourceUri();
if (editorURI && editorURI.path && editorURI.path.ext === '.ino') {
sketchFileURI = editorURI;
}
}
if (sketchFileURI) {
await this.editorManager.open(sketchFileURI);
await this.manager.start(current);
} else {
this.messageService.error('Please open a sketch file to start debugging.')
}
} else {
await this.manager.start(current);
}
}
}
initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
return Promise.resolve();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.editorMode.proMode) {
super.registerMenus(menus);
menus.unregisterMenuAction(DebugCommands.START_NO_DEBUG);
}
}
registerKeybindings(keybindings: KeybindingRegistry): void {
if (this.editorMode.proMode) {
super.registerKeybindings(keybindings);
keybindings.unregisterKeybinding({
command: DebugCommands.START_NO_DEBUG.id,
keybinding: 'ctrl+f5'
});
}
}
registerToolbarItems(toolbar: TabBarToolbarRegistry): void {
super.registerToolbarItems(toolbar);
toolbar.registerItem({
id: ArduinoDebugCommands.START_DEBUG.id,
command: ArduinoDebugCommands.START_DEBUG.id,
tooltip: 'Start Debugging',
priority: 3
});
}
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(ArduinoDebugCommands.START_DEBUG, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => {
registry.executeCommand(DebugCommands.START.id);
}
});
}
}

View File

@@ -1,19 +0,0 @@
import { ContainerModule } from 'inversify';
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
import { ArduinoVariableResolver } from './arduino-variable-resolver';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
import { ArduinoDebugFrontendApplicationContribution } from './arduino-debug-frontend-application-contribution';
import { ArduinoDebugSessionManager } from './arduino-debug-session-manager';
import '../../src/browser/style/index.css';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoVariableResolver).toSelf().inSingletonScope();
bind(VariableContribution).toService(ArduinoVariableResolver);
rebind(DebugSessionManager).to(ArduinoDebugSessionManager).inSingletonScope();
rebind(DebugConfigurationManager).to(ArduinoDebugConfigurationManager).inSingletonScope();
rebind(DebugFrontendApplicationContribution).to(ArduinoDebugFrontendApplicationContribution);
});

View File

@@ -1,14 +0,0 @@
import { DebugSessionManager } from "@theia/debug/lib/browser/debug-session-manager";
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
export class ArduinoDebugSessionManager extends DebugSessionManager {
start(options: DebugSessionOptions) {
if (options.configuration.type === 'arduino' && this.sessions.find(s => s.configuration.type === 'arduino')) {
this.messageService.info('A debug session is already running. You must stop the running session before starting a new one.')
return Promise.resolve(undefined);
}
return super.start(options);
}
}

View File

@@ -1,46 +0,0 @@
import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser';
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { BoardsServiceProvider } from 'arduino-ide-extension/lib/browser/boards/boards-service-provider';
@injectable()
export class ArduinoVariableResolver implements VariableContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MessageService)
protected readonly messageService: MessageService
registerVariables(variables: VariableRegistry): void {
variables.registerVariable(<Variable>{
name: 'fqbn',
description: 'Qualified name of the selected board',
resolve: this.resolveFqbn.bind(this),
});
variables.registerVariable({
name: 'port',
description: 'Selected upload port',
resolve: this.resolvePort.bind(this)
});
}
protected async resolveFqbn(): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceProvider;
if (!boardsConfig || !boardsConfig.selectedBoard) {
this.messageService.error('No board selected. Please select a board for debugging.');
return undefined;
}
return boardsConfig.selectedBoard.fqbn;
}
protected async resolvePort(): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceProvider;
if (!boardsConfig || !boardsConfig.selectedPort) {
return undefined;
}
return boardsConfig.selectedPort.address;
}
}

View File

@@ -1,16 +0,0 @@
.arduino-start-debug-icon {
-webkit-mask: url('debug-dark.svg') 50%;
mask: url('debug-dark.svg') 50%;
-webkit-mask-size: 100%;
mask-size: 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-ui-button-font-color);
}
.arduino-start-debug {
border-radius: 12px;
}

View File

@@ -1,89 +0,0 @@
import * as path from 'path';
import { injectable, inject } from 'inversify';
import { DebugAdapterContribution, DebugAdapterExecutable } from '@theia/debug/lib/common/debug-model';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import { ArduinoDaemonImpl } from 'arduino-ide-extension/lib/node/arduino-daemon-impl';
@injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
readonly type = 'arduino';
readonly label = 'Arduino';
readonly languages = ['c', 'cpp', 'ino'];
@inject(ArduinoDaemonImpl) daemon: ArduinoDaemonImpl;
getSchemaAttributes(): IJSONSchema[] {
return [
{
'properties': {
'sketch': {
'type': 'string',
'description': 'path to the sketch root ino file',
'default': '${file}',
},
'pauseAtMain': {
'description': 'If enabled the debugger will pause at the start of the main function.',
'type': 'boolean',
'default': false
},
'debugDebugAdapter': {
'type': 'boolean',
'description': 'Start the debug adapter in debug mode (with --inspect-brk)',
'default': false
},
}
}
]
}
provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable {
const debugAdapterMain = path.join(__dirname, 'debug-adapter', 'main');
if (config.debugDebugAdapter) {
return {
command: process.execPath,
args: ['--inspect-brk', debugAdapterMain],
}
}
return {
modulePath: debugAdapterMain,
args: [],
}
}
provideDebugConfigurations(): DebugConfiguration[] {
return [
<DebugConfiguration>{
name: this.label,
type: this.type,
request: 'launch'
}
];
}
async resolveDebugConfiguration(config: DebugConfiguration): Promise<DebugConfiguration> {
const startFunction = config.pauseAtMain ? 'main' : 'setup';
const res: ActualDebugConfig = {
...config,
arduinoCli: await this.daemon.getExecPath(),
fqbn: '${fqbn}',
uploadPort: '${port}',
initCommands: [
`-break-insert -t --function ${startFunction}`
]
}
if (!res.sketch) {
res.sketch = '${file}';
}
return res;
}
}
interface ActualDebugConfig extends DebugConfiguration {
arduinoCli?: string;
sketch?: string;
fqbn?: string;
uploadPort?: string;
}

View File

@@ -1,7 +0,0 @@
import { ContainerModule } from 'inversify';
import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model';
import { ArduinoDebugAdapterContribution } from './arduino-debug-adapter-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugAdapterContribution).to(ArduinoDebugAdapterContribution).inSingletonScope();
});

View File

@@ -1,140 +0,0 @@
import { DebugProtocol } from 'vscode-debugprotocol';
import { GDBDebugSession, FrameVariableReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import * as mi from 'cdt-gdb-adapter/dist/mi';
import { ArduinoGDBBackend } from './arduino-gdb-backend';
import { ArduinoVariableHandler } from './arduino-variable-handler';
import { Scope, OutputEvent } from 'vscode-debugadapter';
export interface ArduinoLaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
arduinoCli?: string;
sketch?: string;
fqbn?: string;
uploadPort?: string;
}
const GLOBAL_HANDLE_ID = 0xFE;
const STATIC_HANDLES_START = 0x010000;
const STATIC_HANDLES_FINISH = 0x01FFFF;
export class ArduinoDebugSession extends GDBDebugSession {
private _variableHandler: ArduinoVariableHandler;
get arduinoBackend(): ArduinoGDBBackend {
return this.gdb as ArduinoGDBBackend;
}
protected get variableHandler() {
if (this._variableHandler) {
return this._variableHandler;
}
if (!this.gdb) {
throw new Error("GDB backend is not ready.");
}
const handler = new ArduinoVariableHandler(this, this.frameHandles, this.variableHandles);
this._variableHandler = handler;
return handler;
}
protected createBackend(): GDBBackend {
return new ArduinoGDBBackend();
}
protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): Promise<void> {
try {
await this.gdb.sendCommand('-interpreter-exec console "monitor reset halt"')
await mi.sendExecContinue(this.gdb);
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 100, err.message);
}
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): Promise<void> {
if (process.platform === 'win32') {
const message = 'Pause is not supported on Windows. Please stop the debug session and set a breakpoint instead.';
this.sendEvent(new OutputEvent(message));
this.sendErrorResponse(response, 1, message);
return Promise.resolve();
}
return super.pauseRequest(response, args);
}
protected async disconnectRequest(response: DebugProtocol.DisconnectResponse): Promise<void> {
try {
if (this.isRunning) {
if (process.platform === 'win32') {
// We cannot pause on Windows
this.arduinoBackend.kill();
this.sendResponse(response);
return;
}
// Need to pause first
const waitPromise = new Promise(resolve => this.waitPaused = resolve);
this.gdb.pause();
await waitPromise;
}
await this.gdb.sendGDBExit();
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
try {
const frame: FrameVariableReference = {
type: 'frame',
frameHandle: args.frameId,
};
// const pins: ObjectVariableReference = {
// type: "object",
// varobjName: "__pins",
// frameHandle: 42000,
// }
response.body = {
scopes: [
// new Scope('Pins', this.variableHandles.create(pins), false),
new Scope('Local', this.variableHandles.create(frame), false),
new Scope('Global', GLOBAL_HANDLE_ID, false),
// new Scope('Static', STATIC_HANDLES_START + parseInt(args.frameId as any, 10), false)
],
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> {
try {
response.body = {
variables: [] as DebugProtocol.Variable[]
};
const ref = this.variableHandles.get(args.variablesReference);
if (args.variablesReference === GLOBAL_HANDLE_ID) {
// Use hardcoded global handle to load and store global variables
response.body.variables = await this.variableHandler.getGlobalVariables();
} else if (args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_FINISH) {
// Use STATIC_HANDLES_START to shift the framehandles back
const frameHandle = args.variablesReference - STATIC_HANDLES_START;
response.body.variables = await this.variableHandler.getStaticVariables(frameHandle);
} else if (ref && ref.type === 'frame') {
// List variables for current frame
response.body.variables = await this.handleVariableRequestFrame(ref);
} else if (ref && ref.varobjName === '__pins') {
response.body.variables = await this.variableHandler.handlePinStatusRequest();
} else if (ref && ref.type === 'object') {
// List data under any variable
response.body.variables = await this.handleVariableRequestObject(ref);
}
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
}

View File

@@ -1,73 +0,0 @@
import * as path from 'path';
import * as fs from 'arduino-ide-extension/lib/node/fs-extra'
import { spawn } from 'child_process';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import { MIFrameInfo } from 'cdt-gdb-adapter/dist/mi';
import { ArduinoLaunchRequestArguments } from './arduino-debug-session';
import { ArduinoParser } from './arduino-parser';
export class ArduinoGDBBackend extends GDBBackend {
constructor() {
super();
this.parser = new ArduinoParser(this);
}
spawn(requestArgs: ArduinoLaunchRequestArguments): Promise<void> {
if (!requestArgs.sketch) {
throw new Error('Missing argument: sketch');
}
if (!requestArgs.fqbn) {
throw new Error('Missing argument: fqbn')
}
const sketchFS = fs.statSync(requestArgs.sketch);
const sketchDir = sketchFS.isFile() ? path.dirname(requestArgs.sketch) : requestArgs.sketch;
const command = requestArgs.arduinoCli || 'arduino-cli';
const args = [
'debug',
'-p', requestArgs.uploadPort || 'none',
'-b', requestArgs.fqbn,
'--interpreter', 'mi2',
sketchDir
];
console.log('Starting debugger:', command, JSON.stringify(args));
const proc = spawn(command, args);
this.proc = proc;
this.out = proc.stdin;
return (this.parser as ArduinoParser).parseFull(proc);
}
sendFileExecAndSymbols(): Promise<void> {
// The program file is already sent by `arduino-cli`
return Promise.resolve();
}
sendExecInterrupt(threadId?: number) {
let command = '-exec-interrupt';
if (threadId) {
command += ` --thread ${threadId}`;
}
return this.sendCommand(command);
}
sendStackInfoFrame(threadId: number, frameId: number): Promise<{ frame: MIFrameInfo }> {
const command = `-stack-info-frame --thread ${threadId} --frame ${frameId}`;
return this.sendCommand(command);
}
sendTargetDetach(): Promise<void> {
return this.sendCommand('-target-detach');
}
kill(): void {
if (!this.proc) {
return;
}
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', this.proc.pid.toString(), '/f', '/t']);
} else {
this.proc.kill('SIGKILL');
}
}
}

View File

@@ -1,76 +0,0 @@
import { ChildProcessWithoutNullStreams } from 'child_process';
import { Readable } from 'stream';
import { MIParser } from "cdt-gdb-adapter/dist/MIParser";
const LINE_REGEX = /(.*)(\r?\n)/;
export class ArduinoParser extends MIParser {
protected rejectReady?: (error: Error) => void;
parseFull(proc: ChildProcessWithoutNullStreams): Promise<void> {
return new Promise((resolve, reject) => {
// Detect errors when the child process could not be spawned
proc.on('error', reject);
this.waitReady = () => {
this.rejectReady = undefined;
resolve();
}
this.rejectReady = (error: Error) => {
this.waitReady = undefined;
reject(error);
}
// Detect unexpected termination
proc.on('exit', () => {
if (this.rejectReady) {
this.rejectReady(new Error('The gdb debugger terminated unexpectedly.'));
}
});
this.readInputStream(proc.stdout);
this.readErrorStream(proc.stderr);
});
}
private readInputStream(stream: Readable) {
let buff = '';
stream.on('data', chunk => {
buff += chunk.toString();
let regexArray = LINE_REGEX.exec(buff);
while (regexArray) {
const line = regexArray[1];
this.line = line;
this.pos = 0;
this.handleLine();
// Detect error emitted as log message
if (this.rejectReady && line.toLowerCase().startsWith('&"error')) {
this.pos = 1;
this.rejectReady(new Error(this.handleCString() || regexArray[1]));
this.rejectReady = undefined;
}
buff = buff.substring(regexArray[1].length + regexArray[2].length);
regexArray = LINE_REGEX.exec(buff);
}
});
}
private readErrorStream(stream: Readable) {
let buff = '';
stream.on('data', chunk => {
buff += chunk.toString();
let regexArray = LINE_REGEX.exec(buff);
while (regexArray) {
const line = regexArray[1];
this.gdb.emit('consoleStreamOutput', line + '\n', 'stderr');
// Detect error emitted on the stderr stream
if (this.rejectReady && line.toLowerCase().startsWith('error')) {
this.rejectReady(new Error(line));
this.rejectReady = undefined;
}
buff = buff.substring(regexArray[1].length + regexArray[2].length);
regexArray = LINE_REGEX.exec(buff);
}
});
}
}

View File

@@ -1,115 +0,0 @@
import * as path from 'path';
import { DebugProtocol } from "vscode-debugprotocol";
import { Handles } from 'vscode-debugadapter/lib/handles';
import { FrameReference, VariableReference } from "cdt-gdb-adapter/dist/GDBDebugSession";
import { VarManager } from 'cdt-gdb-adapter/dist/varManager';
import * as mi from 'cdt-gdb-adapter/dist/mi';
import { ArduinoDebugSession } from "./arduino-debug-session";
import { ArduinoGDBBackend } from './arduino-gdb-backend';
export class ArduinoVariableHandler {
protected readonly gdb: ArduinoGDBBackend;
protected readonly varMgr: VarManager;
protected globalHandle: number;
constructor(protected readonly session: ArduinoDebugSession,
protected frameHandles: Handles<FrameReference>,
protected variableHandles: Handles<VariableReference>) {
this.gdb = session.arduinoBackend;
this.varMgr = new VarManager(this.gdb);
}
createGlobalHandle() {
this.globalHandle = this.frameHandles.create({
threadId: -1,
frameId: -1
});
}
/** TODO */
async getGlobalVariables(): Promise<DebugProtocol.Variable[]> {
throw new Error('Global variables are not supported yet.');
const frame = this.frameHandles.get(this.globalHandle);
const symbolInfo: any[] = [] // this.symbolTable.getGlobalVariables();
const variables: DebugProtocol.Variable[] = [];
for (const symbol of symbolInfo) {
const name = `global_var_${symbol.name}`;
const variable = await this.getVariables(frame, name, symbol.name, -1);
variables.push(variable);
}
return variables;
}
/** TODO */
async getStaticVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
throw new Error('Static variables are not supported yet.');
const frame = this.frameHandles.get(frameHandle);
const result = await this.gdb.sendStackInfoFrame(frame.threadId, frame.frameId);
const file = path.normalize(result.frame.file || '');
const symbolInfo: any[] = [] // this.symbolTable.getStaticVariables(file);
const variables: DebugProtocol.Variable[] = [];
// Fetch stack depth to obtain frameId/threadId/depth tuple
const stackDepth = await mi.sendStackInfoDepth(this.gdb, { maxDepth: 100 });
const depth = parseInt(stackDepth.depth, 10);
for (const symbol of symbolInfo) {
const name = `${file}_static_var_${symbol.name}`;
const variable = await this.getVariables(frame, name, symbol.name, depth);
variables.push(variable);
}
return variables;
}
private async getVariables(frame: FrameReference, name: string, expression: string, depth: number): Promise<DebugProtocol.Variable> {
let global = this.varMgr.getVar(frame.frameId, frame.threadId, depth, name);
if (global) {
// Update value if it is already loaded
const vup = await mi.sendVarUpdate(this.gdb, { name });
const update = vup.changelist[0];
if (update && update.in_scope === 'true' && update.name === global.varname) {
global.value = update.value;
}
} else {
// create var in GDB and store it in the varMgr
const varCreateResponse = await mi.sendVarCreate(this.gdb, {
name,
frame: 'current',
expression,
});
global = this.varMgr.addVar(frame.frameId, frame.threadId, depth, name, true, false, varCreateResponse);
}
return {
name: expression,
value: (global.value === void 0) ? '<unknown>' : global.value,
type: global.type,
variablesReference: parseInt(global.numchild, 10) > 0
? this.variableHandles.create({
frameHandle: this.globalHandle,
type: 'object',
varobjName: global.varname,
})
: 0,
};
}
async handlePinStatusRequest(): Promise<DebugProtocol.Variable[]> {
const variables: DebugProtocol.Variable[] = [];
variables.push({
name: "D2",
type: "gpio",
value: "0x00",
variablesReference: 0
})
return variables;
}
}

View File

@@ -1,10 +0,0 @@
import * as process from 'process';
import { logger } from 'vscode-debugadapter/lib/logger';
import { ArduinoDebugSession } from './arduino-debug-session';
import { DebugSession } from 'vscode-debugadapter';
process.on('uncaughtException', (err: any) => {
logger.error(JSON.stringify(err));
});
DebugSession.run(ArduinoDebugSession);

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"noImplicitAny": true,
"noEmitOnError": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"outDir": "lib",
"lib": [
"es6",
"dom"
],
"jsx": "react",
"sourceMap": true,
"skipLibCheck": true
},
"include": [
"src"
],
"files": [
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
]
}

View File

@@ -1,37 +0,0 @@
{
"rules": {
"class-name": true,
"comment-format": [true, "check-space"],
"curly": false,
"forin": false,
"indent": [true, "spaces"],
"max-line-length": [true, 180],
"no-trailing-whitespace": false,
"no-unused-expression": true,
"no-var-keyword": true,
"one-line": [true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"radix": true,
"trailing-comma": [false],
"triple-equals": [true, "allow-null-check"],
"typedef-whitespace": [true, {
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}],
"variable-name": false,
"whitespace": [true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

View File

@@ -1,6 +1,6 @@
## Arduino IDE Extension
Arduino Pro IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
Arduino IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
### IDE Services

View File

@@ -1,137 +0,0 @@
{
"$id": "http://arduino.cc/arduino-cli.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Arduino CLI Configuration",
"properties": {
"board_manager": {
"type": "object",
"description": "Board Manager Configuration",
"properties": {
"additional_urls": {
"type": "array",
"description": "If your board requires 3rd party core packages to work, you can list the URLs to additional package indexes in the Arduino CLI configuration file.",
"items": {
"type": "string",
"description": "URL pointing to the 3rd party core package index JSON.",
"pattern": "^(.*)$"
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"daemon": {
"type": "object",
"description": "CLI Daemon Configuration",
"properties": {
"port": {
"type": [
"string",
"number"
],
"description": "The CLI daemon port where the gRPC clients can connect to.",
"pattern": "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
"additionalProperties": false
}
},
"additionalProperties": false
},
"directories": {
"type": "object",
"description": "Directories Configuration",
"properties": {
"data": {
"type": "string",
"description": "Path to the the data folder where core packages will be stored.",
"pattern": "^(.*)$"
},
"downloads": {
"type": "string",
"description": "Path to the staging folder.",
"pattern": "^(.*)$"
},
"user": {
"type": "string",
"description": "Path to the sketchbooks.",
"pattern": "^(.*)$"
}
},
"additionalProperties": false
},
"logging": {
"type": "object",
"description": "Logging Configuration",
"properties": {
"file": {
"type": "string",
"description": "Path to the file where logs will be written.",
"pattern": "^(.*)$"
},
"format": {
"type": "string",
"description": "The output format for the logs, can be 'text' or 'json'",
"enum": [
"text",
"json"
]
},
"level": {
"type": "string",
"description": "Messages with this level and above will be logged.",
"enum": [
"trace",
"debug",
"info",
"warning",
"error",
"fatal",
"panic"
]
}
},
"additionalProperties": false
},
"sketch": {
"type": "object",
"description": "Sketch Configuration",
"properties": {
"always_export_binaries": {
"type": "boolean",
"description": "Controls whether the compiled binaries will be exported into the sketch's 'build' folder or not. 'false' if the binaries are not exported."
}
},
"additionalProperties": false
},
"telemetry": {
"type": "object",
"description": "Telemetry Configuration",
"properties": {
"addr": {
"type": "string",
"description": "Address to the telemetry endpoint. Must be a full address with host, address, and port. For instance, ':9090' represents 'localhost:9090'",
"pattern": "^(.*)$"
},
"enabled": {
"type": "boolean",
"description": "Whether the telemetry is enabled or not."
},
"additionalProperties": false
},
"additionalProperties": false
},
"library": {
"type": "object",
"description": "Library Configuration",
"properties": {
"enable_unsafe_install": {
"type": "boolean",
"description": "Set to 'true' to enable the use of the '--git-url' and '--zip-file' flags with 'arduino-cli lib install' These are considered 'unsafe' installation methods because they allow installing files that have not passed through the Library Manager submission process."
},
"additionalProperties": false
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "0.1.3",
"version": "2.0.0-beta.2",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"scripts": {
@@ -41,6 +41,7 @@
"@types/ncp": "^2.0.4",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
@@ -56,7 +57,9 @@
"ncp": "^2.0.0",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-disable": "^0.1.0",
"react-select": "^3.0.4",
"react-tabs": "^3.1.2",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
@@ -120,7 +123,7 @@
],
"arduino": {
"cli": {
"version": "0.14.0"
"version": "0.16.0"
}
}
}

View File

@@ -6,7 +6,7 @@
(() => {
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = '9.0.0';
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
const path = require('path');
const shell = require('shelljs');
@@ -22,7 +22,7 @@
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['8.0.1', '9.0.0'],
choices: ['snapshot_20210124'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
@@ -38,35 +38,35 @@
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const alsTarget = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
let clangdTarget, alsSuffix, clangdSuffix;
let clangdExecutablePath, lsSuffix, clangdPrefix;
switch (platform) {
case 'darwin':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'macOS_amd64.zip';
clangdPrefix = 'mac';
break;
case 'linux':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Linux_amd64.zip';
clangdSuffix = 'linux.zip'
clangdExecutablePath = path.join(build, 'bin', 'clangd')
lsSuffix = 'Linux_amd64.zip';
clangdPrefix = 'linux'
break;
case 'win32':
clangdTarget = path.join(build, 'clangd.exe')
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
lsSuffix = 'Windows_amd64.zip';
clangdPrefix = 'windows';
break;
}
if (!alsSuffix) {
if (!lsSuffix) {
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${alsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, alsTarget, force);
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.downloadUnzipAll(clangdUrl, build, clangdTarget, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, { strip: 1 }); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
})();

View File

@@ -79,7 +79,7 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressOptions = undefined) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
@@ -96,12 +96,16 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, targetDir, {
let options = {
plugins: [
unzip(),
untargz()
]
});
};
if (decompressOptions) {
options = Object.assign(options, decompressOptions)
}
const files = await decompress(data, targetDir, options);
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);

View File

@@ -66,24 +66,48 @@
const { platform } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const jsonVersion = shell.exec(`${cli} version --format json`).trim();
if (!jsonVersion) {
const versionJson = shell.exec(`${cli} version --format json`).trim();
if (!versionJson) {
shell.echo(`Could not retrieve the CLI version from ${cli}.`);
shell.exit(1);
}
const version = JSON.parse(jsonVersion).VersionString;
if (version) {
shell.echo(`>>> Checking out version: ${version}...`);
if (shell.exec(`git -C ${repository} checkout ${version} -b ${version}`).code !== 0) {
// As of today (28.01.2021), the `VersionString` can be one of the followings:
// - `nightly-YYYYMMDD` stands for the nightly build, we use the , the `commitish` from the `package.json` to check out the code.
// - `0.0.0-git` for local builds, we use the `commitish` from the `package.json` to check out the code and generate the APIs.
// - `git-snapshot` for local build executed via `task build`. We do not do this.
// - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files.
/*
{
"Application": "arduino-cli",
"VersionString": "nightly-20210126",
"Commit": "079bb6c6",
"Status": "alpha",
"Date": "2021-01-26T01:46:31Z"
}
*/
const versionObject = JSON.parse(versionJson);
const version = versionObject.VersionString;
if (version && !version.startsWith('nightly-') && version !== '0.0.0-git' && version !== 'git-snapshot') {
shell.echo(`>>> Checking out tagged version: '${version}'...`);
shell.exec(`git -C ${repository} fetch --all --tags`);
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out version: ${commitish}.`);
shell.echo(`<<< Checked out tagged version: '${commitish}'.`);
} else if (commitish) {
shell.echo(`>>> Checking out commitish: ${commitish}...`);
shell.echo(`>>> Checking out commitish from 'package.json': '${commitish}'...`);
if (shell.exec(`git -C ${repository} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish: ${commitish}.`);
shell.echo(`<<< Checked out commitish from 'package.json': '${commitish}'.`);
} else if (versionObject.Commit) {
shell.echo(`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`);
if (shell.exec(`git -C ${repository} checkout ${versionObject.Commit}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`);
} else {
shell.echo(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
}
shell.echo('>>> Generating TS/JS API from:');

View File

@@ -20,11 +20,4 @@ export namespace ArduinoCommands {
id: 'arduino-open-boards-dialog'
};
export const TOGGLE_ADVANCED_MODE: Command = {
id: 'arduino-toggle-advanced-mode'
};
export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = {
id: 'arduino-toggle-advanced-mode-toolbar'
};
}

View File

@@ -1,3 +1,4 @@
const debounce = require('lodash.debounce');
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } from '@theia/core';
import {
ContextMenuRenderer,
@@ -23,6 +24,7 @@ import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspac
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
@@ -42,11 +44,10 @@ import { WorkspaceService } from './theia/workspace/workspace-service';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
const debounce = require('lodash.debounce');
import { OutputService } from '../common/protocol/output-service';
import { NotificationCenter } from './notification-center';
import { Settings } from './contributions/settings';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
@injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
@@ -147,11 +148,15 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(ExecutableService)
protected executableService: ExecutableService;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
@@ -192,7 +197,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
viewContribution.initializeLayout(app);
}
}
this.boardsServiceClientImpl.onBoardsConfigChanged(async ({ selectedBoard }) => {
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
@@ -200,20 +205,22 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
this.startLanguageServer(fqbn, name);
}
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.notificationCenter.onConfigChanged(({ config }) => {
if (config) {
this.invalidConfigPopup = undefined;
} else {
if (!this.invalidConfigPopup) {
this.invalidConfigPopup = this.messageService.error(`Your CLI configuration is invalid. Do you want to correct it now?`, 'No', 'Yes')
.then(answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(Settings.Commands.OPEN_CLI_CONFIG.id)
}
this.invalidConfigPopup = undefined;
});
}
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
}
@@ -221,6 +228,14 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
protected startLanguageServer = debounce((fqbn: string, name: string | undefined) => this.doStartLanguageServer(fqbn, name));
protected async doStartLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileSystem.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath] = await Promise.all([
this.fileSystem.fsPath(new URI(clangdUri)),
@@ -231,6 +246,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
board: {
fqbn,
name: name ? `"${name}"` : undefined
@@ -253,12 +269,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor'
});
registry.registerItem({
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id,
tooltip: this.editorMode.proMode ? 'Switch to Classic Mode' : 'Switch to Advanced Mode',
text: this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)'
});
}
registerCommands(registry: CommandRegistry): void {
@@ -279,50 +289,44 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
}
}
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
isToggled: () => this.editorMode.proMode,
execute: () => this.editorMode.toggleProMode()
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => this.editorMode.proMode,
execute: () => this.editorMode.toggleProMode()
});
}
registerMenus(registry: MenuModelRegistry) {
if (!this.editorMode.proMode) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '1'
});
registry.registerMenuAction(CommonMenus.HELP, {
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
label: 'Advanced Mode'
order: '4'
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
for (const uri of [mainFileUri, ...otherSketchFileUris, ...additionalFileUris]) {
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
@@ -362,7 +366,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
light: 'editorWidget.background',
hc: 'editorWidget.background'
},
description: 'Color of the Arduino Pro IDE foreground which is used for dialogs, such as the Select Board dialog.'
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',

View File

@@ -85,7 +85,7 @@ import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './th
import { Contribution } from './contributions/contribution';
import { NewSketch } from './contributions/new-sketch';
import { OpenSketch } from './contributions/open-sketch';
import { CloseSketch } from './contributions/close-sketch';
import { Close } from './contributions/close';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
@@ -130,6 +130,18 @@ import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-de
import { Debug } from './contributions/debug';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { BoardSelection } from './contributions/board-selection';
import { OpenRecentSketch } from './contributions/open-recent-sketch';
import { Help } from './contributions/help';
import { bindArduinoPreferences } from './arduino-preferences'
import { SettingsService, SettingsDialog, SettingsWidget, SettingsDialogProps } from './settings';
import { AddFile } from './contributions/add-file';
import { ArchiveSketch } from './contributions/archive-sketch';
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { OutputToolbarContribution } from './theia/output/output-toolbar-contribution';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@@ -173,6 +185,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SketchesServiceClientImpl);
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
@@ -302,6 +315,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
// No dropdown for the _Output_ view.
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
bind(ArduinoDaemon).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath)).inSingletonScope();
// File-system extension
@@ -315,7 +332,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, NewSketch);
Contribution.configure(bind, OpenSketch);
Contribution.configure(bind, CloseSketch);
Contribution.configure(bind, Close);
Contribution.configure(bind, SaveSketch);
Contribution.configure(bind, SaveAsSketch);
Contribution.configure(bind, VerifySketch);
@@ -331,6 +348,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, IncludeLibrary);
Contribution.configure(bind, About);
Contribution.configure(bind, Debug);
Contribution.configure(bind, Sketchbook);
Contribution.configure(bind, BoardSelection);
Contribution.configure(bind, OpenRecentSketch);
Contribution.configure(bind, Help);
Contribution.configure(bind, AddFile);
Contribution.configure(bind, ArchiveSketch);
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
@@ -339,6 +362,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OutputService).toService(OutputServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);
bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope();
// Enable the dirty indicator on uncloseable widgets.
@@ -361,4 +385,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution);
// Preferences
bindArduinoPreferences(bind);
// Settings wrapper for the preferences and the CLI config.
bind(SettingsService).toSelf().inSingletonScope();
// Settings dialog and widget
bind(SettingsWidget).toSelf().inSingletonScope();
bind(SettingsDialog).toSelf().inSingletonScope();
bind(SettingsDialogProps).toConstantValue({
title: 'Preferences'
});
});

View File

@@ -0,0 +1,73 @@
import { interfaces } from 'inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceContribution,
PreferenceSchema
} from '@theia/core/lib/browser/preferences';
export const ArduinoConfigSchema: PreferenceSchema = {
'type': 'object',
'properties': {
'arduino.language.log': {
'type': 'boolean',
'description': "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
'default': false
},
'arduino.compile.verbose': {
'type': 'boolean',
'description': 'True for verbose compile output. False by default',
'default': false
},
'arduino.upload.verbose': {
'type': 'boolean',
'description': 'True for verbose upload output. False by default.',
'default': false
},
'arduino.upload.verify': {
'type': 'boolean',
'default': false
},
'arduino.window.autoScale': {
'type': 'boolean',
'description': 'True if the user interface automatically scales with the font size.',
'default': true
},
'arduino.window.zoomLevel': {
'type': 'number',
'description': 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
'default': 0
},
'arduino.ide.autoUpdate': {
'type': 'boolean',
'description': 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
'default': true
}
}
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences(preferences: PreferenceService): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema);
}
export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
return createArduinoPreferences(preferences);
});
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema });
}

View File

@@ -1,14 +1,14 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { BoardsServiceProvider } from './boards-service-provider';
import { Board, ConfigOption, Programmer } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@@ -63,7 +63,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
Disposable.create(() => unregisterSubmenu(menuPath, this.menuRegistry)),
...Array.from(commands.keys()).map((commandId, i) => {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
@@ -76,7 +76,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(Disposable.create(() => this.unregisterSubmenu(programmersMenuPath)));
this.toDisposeOnBoardChange.push(Disposable.create(() => unregisterSubmenu(programmersMenuPath, this.menuRegistry)));
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
@@ -98,20 +98,4 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
});
}
protected unregisterSubmenu(menuPath: string[]): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = this.menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
}

View File

@@ -157,7 +157,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {

View File

@@ -57,6 +57,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
protected _availablePorts: Port[] = [];
@@ -187,6 +188,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.latestBoardsConfig = this._boardsConfig;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
@@ -384,7 +386,10 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
await this.storageService.setData(key, selectedBoard);
}
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
await Promise.all([
this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
this.storageService.setData('latest-boards-config', this.latestBoardsConfig)
]);
}
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
@@ -393,15 +398,21 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
const storedLatestValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedLatestValidBoardsConfig) {
this.latestValidBoardsConfig = storedLatestValidBoardsConfig;
if (this.canUploadTo(this.latestValidBoardsConfig)) {
this.boardsConfig = this.latestValidBoardsConfig;
}
} else {
// If we could not restore the latest valid config, try to restore something, the board at least.
const storedLatestBoardsConfig = await this.storageService.getData<BoardsConfig.Config | undefined>('latest-boards-config');
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
this.boardsConfig = this.latestBoardsConfig;
}
}
}
}
/**

View File

@@ -1,15 +1,11 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { BoardsPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
constructor() {
super({
widgetId: BoardsListWidget.WIDGET_ID,
@@ -18,7 +14,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
area: 'left',
rank: 600
},
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+B'
});
}
@@ -27,14 +23,4 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...',
order: '4'
});
}
}
}

View File

@@ -24,31 +24,28 @@ export class About extends Contribution {
}
registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id,
label: `About${isOSX ? ` ${this.applicationName}` : ''}`,
label: `About ${this.applicationName}`,
order: '0'
});
}
async showAbout(): Promise<void> {
const ideStatus = FrontendApplicationConfigProvider.get()['status'];
const { version, commit, status: cliStatus } = await this.configService.getVersion();
const buildDate = this.buildDate;
const detail = (useAgo: boolean) => `
Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${buildDate && useAgo ? ` (${this.ago(buildDate)})` : ''}
const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${buildDate && showAll ? ` (${this.ago(buildDate)})` : ''}
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
Copyright © ${new Date().getFullYear()} Arduino SA
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
`;
const ok = 'OK';
const copy = 'Copy';
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: `${this.applicationName}${ideStatus ? ` ${ideStatus}` : ''}`,
title: `${this.applicationName}${ideStatus ? ` ${ideStatus}` : ''}`,
message: `${this.applicationName}`,
title: `${this.applicationName}`,
type: 'info',
detail: detail(true),
buttons,
@@ -58,7 +55,7 @@ Copyright © ${new Date().getFullYear()} Arduino SA
});
if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false));
await this.clipboardService.writeText(detail(false).trim());
}
}

View File

@@ -0,0 +1,68 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, URI } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id,
label: 'Add File...',
order: '2'
});
}
protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`
});
if (response === 0) { // Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', { timeout: 2000 });
}
}
export namespace AddFile {
export namespace Commands {
export const ADD_FILE: Command = {
id: 'arduino-add-file'
};
}
}

View File

@@ -0,0 +1,55 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
@injectable()
export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch()
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: 'Archive Sketch',
order: '1'
});
}
protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration()
]);
if (!sketch) {
return;
}
const archiveBasename = `${sketch.name}-${dateFormat(new Date(), 'yymmdd')}a.zip`;
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri).resolve(archiveBasename));
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, { timeout: 2000 });
}
}
export namespace ArchiveSketch {
export namespace Commands {
export const ARCHIVE_SKETCH: Command = {
id: 'arduino-archive-sketch'
};
}
}

View File

@@ -0,0 +1,222 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
@injectable()
export class BoardSelection extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } = this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) {
this.messageService.info('Please select a board to obtain board info.');
return;
}
if (!selectedBoard.fqbn) {
this.messageService.info(`The platform for the selected '${selectedBoard.name}' board is not installed.`);
return;
}
if (!selectedPort) {
this.messageService.info('Please select a port to obtain board info.');
return;
}
const boardDetails = await this.boardsService.getBoardDetails({ fqbn: selectedBoard.fqbn });
if (boardDetails) {
const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name}
VID: ${VID}
PID: ${PID}`;
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: 'Board Info',
title: 'Board Info',
type: 'info',
detail,
buttons: ['OK']
});
}
}
});
}
onStart(): void {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(this.updateMenus.bind(this));
this.boardsServiceProvider.onAvailableBoardsChanged(this.updateMenus.bind(this));
}
protected async updateMenus(): Promise<void> {
const [installedBoards, availablePorts, config] = await Promise.all([
this.installedBoards(),
this.boardsService.getState(),
this.boardsServiceProvider.boardsConfig
]);
this.rebuildMenus(installedBoards, availablePorts, config);
}
protected rebuildMenus(installedBoards: InstalledBoardWithPackage[], availablePorts: AvailablePorts, config: BoardsConfig.Config): void {
this.toDisposeBeforeMenuRebuild.dispose();
// Boards submenu
const boardsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards'];
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(boardsSubmenuPath, `Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`, { order: '100' });
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)));
// Ports submenu
const portsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '2_ports'];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(portsSubmenuPath, `Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`, { order: '101' });
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)));
const getBoardInfo = { commandId: BoardSelection.Commands.GET_BOARD_INFO.id, label: 'Get Board Info', order: '103' };
this.menuModelRegistry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, getBoardInfo);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuAction(getBoardInfo)));
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...'
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName);
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () => {
if (fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: {
name,
fqbn,
port: this.boardsServiceProvider.boardsConfig.selectedBoard?.port // TODO: verify!
},
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort
}
}
},
isToggled: () => fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
};
// Board menu
const menuAction = { commandId: id, label: name };
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
}
// Installed ports
const registerPorts = (ports: AvailablePorts) => {
const addresses = Object.keys(ports);
if (!addresses.length) {
return;
}
// Register placeholder for protocol
const [port] = ports[addresses[0]];
const protocol = port.protocol;
const menuPath = [...portsSubmenuPath, protocol];
const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
for (const address of addresses) {
if (!!ports[address]) {
const [port, boards] = ports[address];
if (!boards.length) {
boards.push({
name: ''
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
const command = { id };
const handler = {
execute: () => {
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port
}
}
},
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
};
const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = {
commandId: id,
label,
order: `1${label}` // `1` comes after the placeholder which has order `0`
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
}
}
}
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
this.mainMenuManager.update();
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.allBoards({});
return allBoards.filter(InstalledBoardWithPackage.is);
}
}
export namespace BoardSelection {
export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
}
}

View File

@@ -47,15 +47,19 @@ export class BurnBootloader extends SketchContribution {
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort?.address;
const [fqbn, { selectedProgrammer: programmer }] = await Promise.all([
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose')
]);
this.outputChannelManager.getChannel('Arduino: bootloader').clear();
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
programmer,
port
port,
verify,
verbose
});
this.messageService.info('Done burning bootloader.', { timeout: 1000 });
} catch (e) {

View File

@@ -1,20 +1,50 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { EditorManager } from '@theia/editor/lib/browser';
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, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
/**
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
*/
@injectable()
export class CloseSketch extends SketchContribution {
export class Close extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected shell: ApplicationShell;
onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
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;
@@ -48,7 +78,7 @@ export class CloseSketch extends SketchContribution {
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: CloseSketch.Commands.CLOSE_SKETCH.id,
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5'
});
@@ -56,7 +86,7 @@ export class CloseSketch extends SketchContribution {
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: CloseSketch.Commands.CLOSE_SKETCH.id,
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W'
});
}
@@ -80,10 +110,10 @@ export class CloseSketch extends SketchContribution {
}
export namespace CloseSketch {
export namespace Close {
export namespace Commands {
export const CLOSE_SKETCH: Command = {
id: 'arduino-close-sketch'
export const CLOSE: Command = {
id: 'arduino-close'
};
}
}

View File

@@ -1,20 +1,24 @@
import { inject, injectable, interfaces } from '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';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu';
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { ArduinoPreferences } from '../arduino-preferences';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@@ -39,6 +43,9 @@ export abstract class Contribution implements CommandContribution, MenuContribut
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SettingsService)
protected readonly settingsService: SettingsService;
onStart(app: FrontendApplication): MaybePromise<void> {
}
@@ -77,6 +84,26 @@ export abstract class SketchContribution extends Contribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
override[uri.toString()] = editor.editor.document.getText();
}
}
}
return override;
}
}
export namespace Contribution {

View File

@@ -3,7 +3,6 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { EDITOR_FONT_DEFAULTS } from '@theia/editor/lib/browser/editor-preferences';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -31,10 +30,28 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { execute: () => this.run('editor.action.nextMatchFindAction') });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) + 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) - 1)
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
}
});
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {

View File

@@ -1,14 +1,15 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { MenuPath, SubMenuOptions, CompositeMenuNode } from '@theia/core/lib/common/menu';
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board } from '../../common/protocol';
@injectable()
export abstract class Examples extends SketchContribution {
@@ -32,10 +33,10 @@ export abstract class Examples extends SketchContribution {
@postConstruct()
init(): void {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard?.fqbn));
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
}
protected handleBoardChanged(fqbn: string | undefined): void {
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
@@ -58,28 +59,33 @@ export abstract class Examples extends SketchContribution {
}
registerRecursively(
exampleContainer: ExampleContainer,
exampleContainerOrPlaceholder: ExampleContainer | string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
options?: SubMenuOptions): void {
pushToDispose: DisposableCollection = new DisposableCollection()): void {
const { label, sketches, children } = exampleContainer;
const submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, options);
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) {
const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
const command = { id: commandId };
const handler = {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
}
};
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
if (typeof exampleContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
} else {
const { label, sketches, children } = exampleContainerOrPlaceholder;
const submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label);
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) {
const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
const command = { id: commandId };
const handler = {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
}
};
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
}
}
@@ -102,10 +108,12 @@ export class BuiltInExamples extends Examples {
return;
}
this.toDispose.dispose();
for (const container of exampleContainers) {
for (const container of ['Built-in examples', ...exampleContainers]) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
}
this.menuManager.update();
// TODO: remove
console.log(typeof this.menuRegistry);
}
}
@@ -124,17 +132,27 @@ export class LibraryExamples extends Examples {
this.notificationCenter.onLibraryUninstalled(() => this.register());
}
protected handleBoardChanged(fqbn: string | undefined): void {
this.register(fqbn);
protected handleBoardChanged(board: Board | undefined): void {
this.register(board);
}
protected async register(fqbn: string | undefined = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn) {
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
return this.queue.add(async () => {
this.toDispose.dispose();
if (!fqbn) {
if (!board || !board.fqbn) {
return;
}
const { fqbn, name } = board;
const { user, current, any } = await this.examplesService.installed({ fqbn });
if (user.length) {
(user as any).unshift('Examples from Custom Libraries');
}
if (current.length) {
(current as any).unshift(`Examples for ${name}`);
}
if (any.length) {
(any as any).unshift('Examples for any board');
}
for (const container of user) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
}

View File

@@ -0,0 +1,137 @@
import { inject, injectable } from 'inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandHandler } from '@theia/core/lib/common/command';
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Contribution, Command, MenuModelRegistry, CommandRegistry, KeybindingRegistry } from './contribution';
@injectable()
export class Help extends Contribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void {
const open = (url: string) => this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) => <CommandHandler>{
execute: () => open(url)
};
registry.registerCommand(Help.Commands.GETTING_STARTED, createOpenHandler('https://www.arduino.cc/en/Guide'));
registry.registerCommand(Help.Commands.ENVIRONMENT, createOpenHandler('https://www.arduino.cc/en/Guide/Environment'));
registry.registerCommand(Help.Commands.TROUBLESHOOTING, createOpenHandler('https://support.arduino.cc/hc/en-us'));
registry.registerCommand(Help.Commands.REFERENCE, createOpenHandler('https://www.arduino.cc/reference/en/'));
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
execute: async () => {
let searchFor: string | undefined = undefined;
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
const codeEditor = currentEditor.editor.getControl();
const selection = codeEditor.getSelection();
const model = codeEditor.getModel();
if (model && selection && !monaco.Range.isEmpty(selection)) {
searchFor = model.getValueInRange(selection);
}
}
if (!searchFor) {
searchFor = await this.quickInputService.open({
prompt: 'Search on Arduino.cc',
placeHolder: 'Type a keyword'
});
}
if (searchFor) {
return open(`https://www.arduino.cc/search?q=${encodeURIComponent(searchFor)}&tab=reference`);
}
}
});
registry.registerCommand(Help.Commands.FAQ, createOpenHandler('https://support.arduino.cc/hc/en-us'));
registry.registerCommand(Help.Commands.VISIT_ARDUINO, createOpenHandler('https://www.arduino.cc/'));
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.GETTING_STARTED.id,
order: '0'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.ENVIRONMENT.id,
order: '1'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.TROUBLESHOOTING.id,
order: '2'
});
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.REFERENCE.id,
order: '3'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FIND_IN_REFERENCE.id,
order: '4'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FAQ.id,
order: '5'
});
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.VISIT_ARDUINO.id,
order: '6'
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F'
});
}
}
export namespace Help {
export namespace Commands {
export const GETTING_STARTED: Command = {
id: 'arduino-getting-started',
label: 'Getting Started',
category: 'Arduino'
};
export const ENVIRONMENT: Command = {
id: 'arduino-environment',
label: 'Environment',
category: 'Arduino'
};
export const TROUBLESHOOTING: Command = {
id: 'arduino-troubleshooting',
label: 'Troubleshooting',
category: 'Arduino'
};
export const REFERENCE: Command = {
id: 'arduino-reference',
label: 'Reference',
category: 'Arduino'
};
export const FIND_IN_REFERENCE: Command = {
id: 'arduino-find-in-reference',
label: 'Find in Reference',
category: 'Arduino'
};
export const FAQ: Command = {
id: 'arduino-faq',
label: 'Frequently Asked Questions',
category: 'Arduino'
};
export const VISIT_ARDUINO: Command = {
id: 'arduino-visit-arduino',
label: 'Visit Arduino.cc',
category: 'Arduino'
};
}
}

View File

@@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryPackage, LibraryLocation, LibraryService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { LibraryPackage, LibraryService } from '../../common/protocol';
import { MainMenuManager } from '../../common/main-menu-manager';
import { LibraryListWidget } from '../library/library-list-widget';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
@@ -84,19 +84,35 @@ export class IncludeLibrary extends SketchContribution {
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
for (const library of libraries) {
this.toDispose.push(this.registerLibrary(library, library.location === LibraryLocation.USER ? userMenuPath : packageMenuPath));
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(this.registerLibrary(library, userMenuPath));
}
for (const library of rest) {
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
}
this.mainMenuManager.update();
});
}
protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable {
const commandId = `arduino-include-library--${library.name}:${library.author}`;
protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) };
const menuAction = { commandId, label: library.name };
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),

View File

@@ -0,0 +1,62 @@
import { inject, injectable } from 'inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void {
const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches);
this.mainMenuManager.update();
};
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => refreshMenu(sketches));
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, 'Open Recent', { order: '2' });
}
protected register(sketches: Sketch[]): void {
let order = 0;
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` };
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, { commandId: command.id, label: sketch.name, order: String(order) });
this.toDisposeBeforeRegister.set(sketch.uri, new DisposableCollection(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
));
}
}
}

View File

@@ -122,7 +122,7 @@ export class OpenSketch extends SketchContribution {
filters: [
{
name: 'Sketch',
extensions: ['ino']
extensions: ['ino', 'pde']
}
]
});
@@ -138,7 +138,7 @@ export class OpenSketch extends SketchContribution {
if (sketch) {
return sketch;
}
if (sketchFileUri.endsWith('.ino')) {
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({

View File

@@ -60,8 +60,10 @@ export class SaveAsSketch extends SketchContribution {
}
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
if (workspaceUri && openAfterMove) {
if (wipeOriginal) {
await this.fileService.delete(new URI(sketch.uri));
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
} catch { /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ }
}
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
}

View File

@@ -1,27 +1,49 @@
import { injectable } from 'inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { URI, Command, MenuModelRegistry, CommandRegistry, SketchContribution, open } from './contribution';
import { inject, injectable } from 'inversify';
import { Command, MenuModelRegistry, CommandRegistry, SketchContribution, KeybindingRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings';
@injectable()
export class Settings extends SketchContribution {
@inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog;
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN_CLI_CONFIG, {
execute: () => this.configService.getCliConfigFileUri().then(uri => open(this.openerService, new URI(uri)))
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
try {
this.settingsOpened = true;
settings = await this.settingsDialog.open();
} finally {
this.settingsOpened = false;
}
if (settings) {
await this.settingsService.update(settings);
await this.settingsService.save();
} else {
await this.settingsService.reset();
}
},
isEnabled: () => !this.settingsOpened
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: CommonCommands.OPEN_PREFERENCES.id,
commandId: Settings.Commands.OPEN.id,
label: 'Preferences...',
order: '0'
});
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
commandId: Settings.Commands.OPEN_CLI_CONFIG.id,
label: 'Open CLI Configuration',
order: '1',
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
});
}
@@ -29,9 +51,9 @@ export class Settings extends SketchContribution {
export namespace Settings {
export namespace Commands {
export const OPEN_CLI_CONFIG: Command = {
id: 'arduino-open-cli-config',
label: 'Open CLI Configuration',
export const OPEN: Command = {
id: 'arduino-settings-open',
label: 'Open Preferences...',
category: 'Arduino'
}
}

View File

@@ -40,8 +40,8 @@ export class SketchControl extends SketchContribution {
return;
}
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
const { mainFileUri, rootFolderFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...rootFolderFileUris];
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
const command = { id: `arduino-focus-file--${uri.toString()}` };

View File

@@ -0,0 +1,69 @@
import { inject, injectable } from 'inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { OpenSketch } from './open-sketch';
@injectable()
export class Sketchbook extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposePerSketch = new Map<string, DisposableCollection>();
onStart(): void {
this.sketchService.getSketches().then(sketches => {
this.register(sketches);
this.mainMenuManager.update();
});
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
this.unregister(removed);
this.register(created);
this.mainMenuManager.update();
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
}
protected register(sketches: Sketch[]): void {
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-sketchbook-open--${uri}` };
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
));
}
}
protected unregister(sketches: Sketch[]): void {
for (const { uri } of sketches) {
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
}
}
}

View File

@@ -43,12 +43,12 @@ export class UploadSketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '0'
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '1'
order: '2'
});
}
@@ -73,8 +73,8 @@ export class UploadSketch extends SketchContribution {
}
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
let shouldAutoConnect = false;
@@ -88,13 +88,16 @@ export class UploadSketch extends SketchContribution {
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }] = await Promise.all([
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
this.sourceOverride()
]);
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = uri;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort?.address;
@@ -106,17 +109,23 @@ export class UploadSketch extends SketchContribution {
fqbn,
optimizeForDebug,
programmer,
port
port,
verbose,
verify,
sourceOverride
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port
port,
verbose,
verify,
sourceOverride
};
}
this.outputChannelManager.getChannel('Arduino: upload').clear();
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {

View File

@@ -26,6 +26,9 @@ export class VerifySketch extends SketchContribution {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch()
});
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true)
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id)
@@ -36,7 +39,12 @@ export class VerifySketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
order: '2'
order: '0'
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
label: 'Export compiled Binary',
order: '3'
});
}
@@ -45,6 +53,10 @@ export class VerifySketch extends SketchContribution {
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R'
});
registry.registerKeybinding({
command: VerifySketch.Commands.EXPORT_BINARIES.id,
keybinding: 'CtrlCmd+Alt+S'
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -56,19 +68,26 @@ export class VerifySketch extends SketchContribution {
});
}
async verifySketch(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
async verifySketch(exportBinaries?: boolean): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn);
this.outputChannelManager.getChannel('Arduino: compile').clear();
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
this.sourceOverride()
]);
const verbose = this.preferences.get('arduino.compile.verbose');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: uri,
sketchUri: sketch.uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride
});
this.messageService.info('Done compiling.', { timeout: 1000 });
} catch (e) {
@@ -83,6 +102,9 @@ export namespace VerifySketch {
export const VERIFY_SKETCH: Command = {
id: 'arduino-verify-sketch'
};
export const EXPORT_BINARIES: Command = {
id: 'arduino-export-binaries'
};
export const VERIFY_SKETCH_TOOLBAR: Command = {
id: 'arduino-verify-sketch--toolbar'
};

View File

@@ -1,10 +1,6 @@
import { injectable, inject } from 'inversify';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsListWidget } from './boards/boards-list-widget';
import { LibraryListWidget } from './library/library-list-widget';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
@@ -16,41 +12,6 @@ export class EditorMode implements FrontendApplicationContribution {
onStart(app: FrontendApplication): void {
this.app = app;
if (this.proMode) {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(EditorMode.PRO_MODE_KEY);
}
}
get proMode(): boolean {
const value = window.localStorage.getItem(EditorMode.PRO_MODE_KEY);
return value === 'true';
}
async toggleProMode(): Promise<void> {
const oldState = this.proMode;
const inAdvancedMode = !oldState;
window.localStorage.setItem(EditorMode.PRO_MODE_KEY, String(inAdvancedMode));
if (!inAdvancedMode) {
const { shell } = this.app;
// Close all widgets that are neither editor nor `Output` / `Boards Manager` / `Library Manager`.
for (const area of ['left', 'right', 'bottom', 'main'] as Array<ApplicationShell.Area>) {
shell.closeTabs(area, title => !this.isInSimpleMode(title.owner));
}
}
// `storeLayout` has a sync API but the implementation is async, we store the layout manually before we reload the page.
// See: https://github.com/eclipse-theia/theia/issues/6579
// XXX: hack instead of injecting the `ArduinoShellLayoutRestorer` we have to retrieve it from the application to avoid DI cycle.
const layoutRestorer = (this.app as any).layoutRestorer as { storeLayoutAsync(app: FrontendApplication): Promise<void> };
await layoutRestorer.storeLayoutAsync(this.app);
window.location.reload(true);
}
protected isInSimpleMode(widget: Widget): boolean {
return widget instanceof EditorWidget
|| widget instanceof OutputWidget
|| widget instanceof BoardsListWidget
|| widget instanceof LibraryListWidget;
}
get compileForDebug(): boolean {
@@ -68,6 +29,5 @@ export class EditorMode implements FrontendApplicationContribution {
}
export namespace EditorMode {
export const PRO_MODE_KEY = 'arduino-advanced-mode';
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
}

View File

@@ -1,6 +1,6 @@
import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { MAIN_MENU_BAR } from '@theia/core/lib/common/menu';
import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode, MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
export namespace ArduinoMenus {
@@ -12,8 +12,14 @@ export namespace ArduinoMenus {
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
// -- File / Open Recent
export const FILE__OPEN_RECENT_SUBMENU = [...FILE__SKETCH_GROUP, '0_open_recent'];
// -- File / Sketchbook
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '1_sketchbook'];
// -- File / Examples
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '0_examples'];
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples'];
export const EXAMPLES__BUILT_IN_GROUP = [...FILE__EXAMPLES_SUBMENU, '0_built_ins'];
export const EXAMPLES__ANY_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '1_any_board'];
export const EXAMPLES__CURRENT_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '2_current_board'];
@@ -37,10 +43,19 @@ export namespace ArduinoMenus {
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// `Board`, `Port`, and `Get Board Info`.
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '1_board_settings'];
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main'];
// `Find in reference`, `FAQ`, etc.
export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find'];
// `Advanced Mode`.
// XXX: this will be removed.
export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control'];
// `About` group
// XXX: on macOS, the about group is not under `Help`
export const HELP__ABOUT_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP), '999_about'];
@@ -64,3 +79,44 @@ export namespace ArduinoMenus {
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [...SKETCH_CONTROL__CONTEXT, '2_resources'];
}
/**
* This is a hack. It removes a submenu with all its children if any.
* Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299
*/
export function unregisterSubmenu(menuPath: string[], menuRegistry: MenuModelRegistry): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements MenuNode {
constructor(protected readonly menuPath: MenuPath, readonly label: string, protected options: SubMenuOptions = { order: '0' }) { }
get icon(): string | undefined {
return this.options?.iconClass;
}
get sortString(): string {
return this.options?.order || this.label;
}
get id(): string {
return [...this.menuPath, 'placeholder'].join('-');
}
}

View File

@@ -4,7 +4,7 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { NotificationServiceClient, NotificationServiceServer } from '../common/protocol/notification-service';
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol';
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
@injectable()
export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution {
@@ -21,6 +21,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[] }>();
protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter,
@@ -43,6 +44,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
@postConstruct()
protected init(): void {
@@ -89,4 +91,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
this.attachedBoardsChangedEmitter.fire(event);
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.recentSketchesChangedEmitter.fire(event);
}
}

View File

@@ -13,16 +13,10 @@ export class OutputServiceImpl implements OutputService {
protected outputChannelManager: OutputChannelManager;
append(message: OutputMessage): void {
const { name, chunk } = message;
const channel = this.outputChannelManager.getChannel(`Arduino: ${name}`);
// Zen-mode: we do not reveal the output for daemon messages.
const show: Promise<any> = name === 'daemon'
// This will open and reveal the view but won't show it. You will see the toggle bottom panel on the status bar.
? this.outputContribution.openView({ activate: false, reveal: false })
// This will open, reveal but do not activate the Output view.
: Promise.resolve(channel.show({ preserveFocus: true }));
show.then(() => channel.append(chunk));
const { chunk } = message;
const channel = this.outputChannelManager.getChannel(`Arduino`);
channel.show({ preserveFocus: true });
channel.append(chunk);
}
}

View File

@@ -0,0 +1,791 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import { Disable } from 'react-disable';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { MaybePromise } from '@theia/core/lib/common/types';
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 { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { AbstractDialog, DialogProps, PreferenceService, PreferenceScope, DialogError, ReactWidget } from '@theia/core/lib/browser';
import { Index } from '../common/types';
import { ConfigService, FileSystemExt, Network, ProxySettings } from '../common/protocol';
export interface Settings extends Index {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: 'on' | 'off'; // `editor.autoSave`
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
checkForUpdates?: boolean; // `arduino.ide.autoUpdate`
verboseOnCompile: boolean; // `arduino.compile.verbose`
verboseOnUpload: boolean; // `arduino.upload.verbose`
verifyAfterUpload: boolean; // `arduino.upload.verify`
enableLsLogs: boolean; // `arduino.language.log`
sketchbookPath: string; // CLI
additionalUrls: string[]; // CLI
network: Network; // CLI
}
export namespace Settings {
export function belongsToCli<K extends keyof Settings>(key: K): boolean {
return key === 'sketchbookPath' || key === 'additionalUrls';
}
}
@injectable()
export class SettingsService {
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected ready = new Deferred<void>();
protected _settings: Settings;
@postConstruct()
protected async init(): Promise<void> {
await this.appStateService.reachedState('ready'); // Hack for https://github.com/eclipse-theia/theia/issues/8993
const settings = await this.loadSettings();
this._settings = deepClone(settings);
this.ready.resolve();
}
protected async loadSettings(): Promise<Settings> {
await this.preferenceService.ready;
const [
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
cliConfig
] = await Promise.all([
this.preferenceService.get<number>('editor.fontSize', 12),
this.preferenceService.get<string>('workbench.colorTheme', 'arduino-theme'),
this.preferenceService.get<'on' | 'off'>('editor.autoSave', 'on'),
this.preferenceService.get<boolean>('arduino.window.autoScale', true),
this.preferenceService.get<number>('arduino.window.zoomLevel', 0),
// this.preferenceService.get<string>('arduino.ide.autoUpdate', true),
this.preferenceService.get<boolean>('arduino.compile.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verify', true),
this.preferenceService.get<boolean>('arduino.language.log', true),
this.configService.getConfiguration()
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
return {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
additionalUrls,
sketchbookPath,
network
};
}
async settings(): Promise<Settings> {
await this.ready.promise;
return this._settings;
}
async update(settings: Settings, fireDidChange: boolean = false): Promise<void> {
await this.ready.promise;
for (const key of Object.keys(settings)) {
this._settings[key] = settings[key];
}
if (fireDidChange) {
this.onDidChangeEmitter.fire(this._settings);
}
}
async reset(): Promise<void> {
const settings = await this.loadSettings();
return this.update(settings, true);
}
async validate(settings: MaybePromise<Settings> = this.settings()): Promise<string | true> {
try {
const { sketchbookPath, editorFontSize, themeId } = await settings;
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
if (!await this.fileService.exists(new URI(sketchbookDir))) {
return `Invalid sketchbook location: ${sketchbookPath}`;
}
if (editorFontSize <= 0) {
return `Invalid editor font size. It must be a positive integer.`;
}
if (!ThemeService.get().getThemes().find(({ id }) => id === themeId)) {
return `Invalid theme.`;
}
return true;
} catch (err) {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
}
async save(): Promise<string | true> {
await this.ready.promise;
const {
editorFontSize,
themeId,
autoSave,
autoScaleInterface,
interfaceScale,
// checkForUpdates,
verboseOnCompile,
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
sketchbookPath,
additionalUrls,
network
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
this.fileSystemExt.getUri(sketchbookPath)
]);
(config as any).additionalUrls = additionalUrls;
(config as any).sketchDirUri = sketchDirUri;
(config as any).network = network;
await Promise.all([
this.preferenceService.set('editor.fontSize', editorFontSize, PreferenceScope.User),
this.preferenceService.set('workbench.colorTheme', themeId, PreferenceScope.User),
this.preferenceService.set('editor.autoSave', autoSave, PreferenceScope.User),
this.preferenceService.set('arduino.window.autoScale', autoScaleInterface, PreferenceScope.User),
this.preferenceService.set('arduino.window.zoomLevel', interfaceScale, PreferenceScope.User),
// this.preferenceService.set('arduino.ide.autoUpdate', checkForUpdates, PreferenceScope.User),
this.preferenceService.set('arduino.compile.verbose', verboseOnCompile, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verbose', verboseOnUpload, PreferenceScope.User),
this.preferenceService.set('arduino.upload.verify', verifyAfterUpload, PreferenceScope.User),
this.preferenceService.set('arduino.language.log', enableLsLogs, PreferenceScope.User),
this.configService.setConfiguration(config)
]);
this.onDidChangeEmitter.fire(this._settings);
return true;
}
}
export class SettingsComponent extends React.Component<SettingsComponent.Props, SettingsComponent.State> {
readonly toDispose = new DisposableCollection();
constructor(props: SettingsComponent.Props) {
super(props);
}
componentDidUpdate(_: SettingsComponent.Props, prevState: SettingsComponent.State): void {
if (this.state && prevState && JSON.stringify(this.state) !== JSON.stringify(prevState)) {
this.props.settingsService.update(this.state, true);
}
}
componentDidMount(): void {
this.props.settingsService.settings().then(settings => this.setState(settings));
this.toDispose.push(this.props.settingsService.onDidChange(settings => this.setState(settings)));
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
if (!this.state) {
return <div />;
}
return <Tabs>
<TabList>
<Tab>Settings</Tab>
<Tab>Network</Tab>
</TabList>
<TabPanel>
{this.renderSettings()}
</TabPanel>
<TabPanel>
{this.renderNetwork()}
</TabPanel>
</Tabs>;
}
protected renderSettings(): React.ReactNode {
return <div className='content noselect'>
Sketchbook location:
<div className='flex-line'>
<input
className='theia-input stretch'
type='text'
value={this.state.sketchbookPath}
onChange={this.sketchpathDidChange} />
<button className='theia-button shrink' onClick={this.browseSketchbookDidClick}>Browse</button>
</div>
<div className='flex-line'>
<div className='column'>
<div className='flex-line'>Editor font size:</div>
<div className='flex-line'>Interface scale:</div>
<div className='flex-line'>Theme:</div>
<div className='flex-line'>Show verbose output during:</div>
</div>
<div className='column'>
<div className='flex-line'>
<input
className='theia-input small'
type='number'
step={1}
pattern='[0-9]+'
onKeyDown={this.numbersOnlyKeyDown}
value={this.state.editorFontSize}
onChange={this.editorFontSizeDidChange} />
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoScaleInterface}
onChange={this.autoScaleInterfaceDidChange} />
Automatic
</label>
<input
className='theia-input small with-margin'
type='number'
step={20}
pattern='[0-9]+'
onKeyDown={this.noopKeyDown}
value={100 + this.state.interfaceScale * 20}
onChange={this.interfaceScaleDidChange} />
%
</div>
<div className='flex-line'>
<select
className='theia-select'
value={ThemeService.get().getThemes().find(({ id }) => id === this.state.themeId)?.label || 'Unknown'}
onChange={this.themeDidChange}>
{ThemeService.get().getThemes().map(({ id, label }) => <option key={id} value={label}>{label}</option>)}
</select>
</div>
<div className='flex-line'>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnCompile}
onChange={this.verboseOnCompileDidChange} />
compile
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verboseOnUpload}
onChange={this.verboseOnUploadDidChange} />
upload
</label>
</div>
</div>
</div>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.verifyAfterUpload}
onChange={this.verifyAfterUploadDidChange} />
Verify code after upload
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.checkForUpdates}
onChange={this.checkForUpdatesDidChange}
disabled={true} />
Check for updates on startup
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.autoSave === 'on'}
onChange={this.autoSaveDidChange} />
Auto save
</label>
<label className='flex-line'>
<input
type='checkbox'
checked={this.state.enableLsLogs}
onChange={this.enableLsLogsDidChange} />
Enable language server logging
</label>
<div className='flex-line'>
Additional boards manager URLs:
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.additionalUrls.join(',')}
onChange={this.additionalUrlsDidChange} />
<i className='fa fa-window-restore theia-button shrink' onClick={this.editAdditionalUrlDidClick} />
</div>
</div>;
}
protected renderNetwork(): React.ReactNode {
return <div className='content noselect'>
<form>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none'}
onChange={this.noProxyDidChange} />
No proxy
</label>
<label className='flex-line'>
<input
type='radio'
checked={this.state.network !== 'none'}
onChange={this.manualProxyDidChange} />
Manual proxy configuration
</label>
</form>
{this.renderProxySettings()}
</div>;
}
protected renderProxySettings(): React.ReactNode {
const disabled = this.state.network === 'none';
return <Disable disabled={disabled}>
<div className='proxy-settings' aria-disabled={disabled}>
<form className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? true : this.state.network.protocol === 'http'}
onChange={this.httpProtocolDidChange} />
HTTP
<label className='flex-line'>
<input
type='radio'
checked={this.state.network === 'none' ? false : this.state.network.protocol !== 'http'}
onChange={this.socksProtocolDidChange} />
SOCKS
</label>
</form>
<div className='flex-line proxy-settings'>
<div className='column'>
<div className='flex-line'>Host name:</div>
<div className='flex-line'>Port number:</div>
<div className='flex-line'>Username:</div>
<div className='flex-line'>Password:</div>
</div>
<div className='column stretch'>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.hostname}
onChange={this.hostnameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input small with-margin'
type='number'
pattern='[0-9]'
value={this.state.network === 'none' ? '' : this.state.network.port}
onKeyDown={this.numbersOnlyKeyDown}
onChange={this.portDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='text'
value={this.state.network === 'none' ? '' : this.state.network.username}
onChange={this.usernameDidChange} />
</div>
<div className='flex-line'>
<input
className='theia-input stretch with-margin'
type='password'
value={this.state.network === 'none' ? '' : this.state.network.password}
onChange={this.passwordDidChange} />
</div>
</div>
</div>
</div>
</Disable>;
}
private isControlKey(event: React.KeyboardEvent<HTMLInputElement>): boolean {
return !!event.key && ['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some(key => event.key.toLocaleLowerCase() === key);
}
protected noopKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
}
protected numbersOnlyKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (this.isControlKey(event)) {
return;
}
const key = Number(event.key)
if (isNaN(key) || event.key === null || event.key === ' ') {
event.nativeEvent.preventDefault();
event.nativeEvent.returnValue = false;
return;
}
}
protected browseSketchbookDidClick = async () => {
const uri = await this.props.fileDialogService.showOpenDialog({
title: 'Select new sketchbook location',
openLabel: 'Chose',
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
this.setState({ sketchbookPath });
}
};
protected editAdditionalUrlDidClick = async () => {
const additionalUrls = await new AdditionalUrlsDialog(this.state.additionalUrls, this.props.windowService).open();
if (additionalUrls) {
this.setState({ additionalUrls });
}
};
protected editorFontSizeDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (value) {
this.setState({ editorFontSize: parseInt(value, 10) });
}
};
protected additionalUrlsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ additionalUrls: event.target.value.split(',').map(url => url.trim()) });
};
protected autoScaleInterfaceDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoScaleInterface: event.target.checked });
};
protected enableLsLogsDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ enableLsLogs: event.target.checked });
};
protected interfaceScaleDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
const percentage = parseInt(value, 10);
if (isNaN(percentage)) {
return;
}
let interfaceScale = (percentage - 100) / 20;
if (!isNaN(interfaceScale)) {
this.setState({ interfaceScale });
}
};
protected verifyAfterUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verifyAfterUpload: event.target.checked });
};
protected checkForUpdatesDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ checkForUpdates: event.target.checked });
};
protected autoSaveDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autoSave: event.target.checked ? 'on' : 'off' });
};
protected themeDidChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
}
};
protected verboseOnCompileDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnCompile: event.target.checked });
};
protected verboseOnUploadDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ verboseOnUpload: event.target.checked });
};
protected sketchpathDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const sketchbookPath = event.target.value;
if (sketchbookPath) {
this.setState({ sketchbookPath });
}
};
protected noProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: 'none' });
} else {
this.setState({ network: Network.Default() });
}
};
protected manualProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
this.setState({ network: Network.Default() });
} else {
this.setState({ network: 'none' });
}
};
protected httpProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'http' : 'socks';
this.setState({ network });
}
};
protected socksProtocolDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.protocol = event.target.checked ? 'socks' : 'http';
this.setState({ network });
}
};
protected hostnameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.hostname = event.target.value;
this.setState({ network });
}
};
protected portDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.port = event.target.value;
this.setState({ network });
}
};
protected usernameDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.username = event.target.value;
this.setState({ network });
}
};
protected passwordDidChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (this.state.network !== 'none') {
const network = this.cloneProxySettings;
network.password = event.target.value;
this.setState({ network });
}
};
private get cloneProxySettings(): ProxySettings {
const { network } = this.state;
if (network === 'none') {
throw new Error('Must be called when proxy is enabled.');
}
const copyNetwork = deepClone(network);
return copyNetwork;
}
}
export namespace SettingsComponent {
export interface Props {
readonly settingsService: SettingsService;
readonly fileService: FileService;
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
}
export interface State extends Settings { }
}
@injectable()
export class SettingsWidget extends ReactWidget {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(WindowService)
protected readonly windowService: WindowService;
protected render(): React.ReactNode {
return <SettingsComponent
settingsService={this.settingsService}
fileService={this.fileService}
fileDialogService={this.fileDialogService}
windowService={this.windowService} />;
}
}
@injectable()
export class SettingsDialogProps extends DialogProps {
}
@injectable()
export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
constructor(@inject(SettingsDialogProps) protected readonly props: SettingsDialogProps) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.settingsService.onDidChange(this.validate.bind(this)));
}
protected async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
}
return '';
}
get value(): Promise<Settings> {
return this.settingsService.settings();
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.settingsService.onDidChange(() => this.update()));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}
export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
protected readonly textArea: HTMLTextAreaElement;
constructor(urls: string[], windowService: WindowService) {
super({ title: 'Additional Boards Manager URLs' });
this.contentNode.classList.add('additional-urls-dialog');
const description = document.createElement('div');
description.textContent = 'Enter additional URLs, one for each row';
description.style.marginBottom = '5px';
this.contentNode.appendChild(description);
this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls.filter(url => url.trim()).filter(url => !!url).join('\n');
this.textArea.wrap = 'soft';
this.textArea.cols = 90;
this.textArea.rows = 5;
this.contentNode.appendChild(this.textArea);
const anchor = document.createElement('div');
anchor.classList.add('link');
anchor.textContent = 'Click for a list of unofficial board support URLs';
anchor.style.marginTop = '5px';
anchor.style.cursor = 'pointer';
this.addEventListener(
anchor,
'click',
() => windowService.openNewWindow('https://github.com/arduino/Arduino/wiki/Unofficial-list-of-3rd-party-boards-support-urls', { external: true })
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton('OK');
this.appendCloseButton('Cancel');
}
get value(): string[] {
return this.textArea.value.split('\n').map(url => url.trim());
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}
return false;
}
}

View File

@@ -135,18 +135,6 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
width: 740px;
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-foreground);
}
button.theia-button.main {
color: var(--theia-button-foreground);
}
.dialogControl {
margin: 0 20px 30px 0;
@@ -203,12 +191,14 @@ button.theia-button.main {
width: 100%;
overflow: hidden;
margin: 0px 3px 0px 3px;
border: 1px solid var(--theia-dropdown-border);
}
.arduino-boards-dropdown-list {
border: 3px solid var(--theia-activityBar-background);
margin: -3px;
margin: -1px;
z-index: 1;
border: 1px solid var(--theia-dropdown-border);
}
.arduino-boards-dropdown-item {
@@ -218,6 +208,7 @@ button.theia-button.main {
cursor: pointer;
color: var(--theia-foreground);
background: var(--theia-tab-unfocusedActiveBackground);
border: 1px solid var(--theia-tab-unfocusedActiveBackground);
}
.arduino-boards-dropdown-item .fa-check {
@@ -227,5 +218,5 @@ button.theia-button.main {
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-list-hoverBackground);
border: 1px solid var(--theia-focusBorder);
}

View File

@@ -6,6 +6,7 @@
@import './status-bar.css';
@import './terminal.css';
@import './editor.css';
@import './settings-dialog.css';
.theia-input.warning:focus {
outline-width: 1px;
@@ -35,3 +36,28 @@
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
/* Overrule the default Theia CSS button styles. */
button.theia-button,
.theia-button {
border: 1px solid var(--theia-dropdown-border);
}
button.theia-button:hover,
.theia-button:hover {
border: 1px solid var(--theia-focusBorder);
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-foreground);
}
button.theia-button.main {
color: var(--theia-button-foreground);
}

View File

@@ -0,0 +1,57 @@
.arduino-settings-dialog {
width: 740px;
}
.arduino-settings-dialog .content {
padding: 5px;
height: 250px;
}
.arduino-settings-dialog .flex-line {
display: flex;
align-items: center;
white-space: nowrap;
}
.arduino-settings-dialog .with-margin {
margin-left: 5px;
}
.arduino-settings-dialog .theia-select {
background: var(--theia-input-background) !important;
}
.arduino-settings-dialog .column > div {
height: 26px;
vertical-align: middle;
}
.arduino-settings-dialog .stretch {
width: 100% !important;
}
.arduino-settings-dialog .flex-line .theia-button.shrink {
min-width: unset;
}
.arduino-settings-dialog .proxy-settings {
margin: 5px;
}
.arduino-settings-dialog input[type="radio"] {
margin: 3px !important;
}
.arduino-settings-dialog .theia-input.small {
max-width: 50px;
width: 50px;
}
.additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground);
}
.arduino-settings-dialog .react-tabs__tab-list {
display: flex;
justify-content: center;
}

View File

@@ -7,16 +7,12 @@ import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { ConnectionStatusService, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service';
import { ApplicationShell as TheiaApplicationShell, Widget } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { EditorMode } from '../../editor-mode';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(CommandService)
protected readonly commandService: CommandService;
@@ -34,7 +30,7 @@ export class ApplicationShell extends TheiaApplicationShell {
if (widget instanceof OutputWidget) {
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
}
if (!this.editorMode.proMode && widget instanceof EditorWidget) {
if (widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then(sketch => {
if (sketch) {

View File

@@ -20,7 +20,8 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.OPEN_PREFERENCES,
CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME,
CommonCommands.ABOUT_COMMAND
CommonCommands.ABOUT_COMMAND,
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877
]) {
registry.unregisterMenuAction(command);
}

View File

@@ -3,6 +3,7 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CommandService } from '@theia/core/lib/common/command';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SketchesService } from '../../../common/protocol';
import { ArduinoCommands } from '../../arduino-commands';
@injectable()
@@ -17,12 +18,16 @@ export class FrontendApplication extends TheiaFrontendApplication {
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
protected async initializeLayout(): Promise<void> {
await super.initializeLayout();
const roots = await this.workspaceService.roots;
for (const root of roots) {
const exists = await this.fileService.exists(root.resource);
if (exists) {
this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.resource);
}
}

View File

@@ -0,0 +1,14 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution, DebugMenus } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { unregisterSubmenu } from '../../menu/arduino-menus';
@injectable()
export class DebugFrontendApplicationContribution extends TheiaDebugFrontendApplicationContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
unregisterSubmenu(DebugMenus.DEBUG, registry);
}
}

View File

@@ -24,13 +24,7 @@ export class EditorManager extends TheiaEditorManager {
}
protected async isReadOnly(uri: URI): Promise<boolean> {
const [config, configFileUri] = await Promise.all([
this.configService.getConfiguration(),
this.configService.getCliConfigFileUri()
]);
if (new URI(configFileUri).toString(true) === uri.toString(true)) {
return false;
}
const config = await this.configService.getConfiguration();
return new URI(config.dataDirUri).isEqualOrParent(uri)
}

View File

@@ -1,26 +1,18 @@
import { inject, injectable } from 'inversify';
import { injectable } from 'inversify';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ProblemContribution as TheiaProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class ProblemContribution extends TheiaProblemContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
// NOOP
}
protected setStatusBarElement(problemStat: ProblemStat): void {
if (this.editorMode.proMode) {
super.setStatusBarElement(problemStat);
}
// NOOP
}
registerKeybindings(keybindings: KeybindingRegistry): void {

View File

@@ -1,20 +1,14 @@
import { injectable, inject } from 'inversify';
import { injectable } from 'inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class FileNavigatorContribution extends TheiaFileNavigatorContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
// NOOP
}
registerKeybindings(registry: KeybindingRegistry): void {

View File

@@ -1,14 +1,10 @@
import { injectable, inject } from 'inversify';
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { EditorMode } from '../../editor-mode';
@injectable()
export class OutlineViewContribution extends TheiaOutlineViewContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
constructor() {
super();
this.options.defaultWidgetOptions = {
@@ -18,9 +14,7 @@ export class OutlineViewContribution extends TheiaOutlineViewContribution {
}
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
// NOOP
}
}

View File

@@ -0,0 +1,15 @@
import { injectable } from 'inversify';
import { ReactTabBarToolbarItem, TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
@injectable()
export class OutputToolbarContribution extends TheiaOutputToolbarContribution {
async registerToolbarItems(registry: TabBarToolbarRegistry): Promise<void> {
await super.registerToolbarItems(registry); // Why is it async?
// It's a hack. Currently, it's not possible to unregister a toolbar contribution via API.
((registry as any).items as Map<string, TabBarToolbarItem | ReactTabBarToolbarItem>).delete('channels');
(registry as any).fireOnDidChange();
}
}

View File

@@ -15,11 +15,7 @@ export class PreferencesContribution extends TheiaPreferencesContribution {
}
registerKeybindings(registry: KeybindingRegistry): void {
// https://github.com/eclipse-theia/theia/issues/8202
registry.registerKeybinding({
command: CommonCommands.OPEN_PREFERENCES.id,
keybinding: 'CtrlCmd+,',
});
registry.unregisterKeybinding(CommonCommands.OPEN_PREFERENCES.id);
}
}

View File

@@ -1,24 +1,16 @@
import { inject, injectable } from 'inversify';
import { injectable } from 'inversify';
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { EditorMode } from '../../editor-mode';
@injectable()
export class ScmContribution extends TheiaScmContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
// NOOP
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
if (this.editorMode.proMode) {
super.setStatusBarEntry(id, entry);
}
// NOOP
}
}

View File

@@ -1,19 +1,14 @@
import { inject, injectable } from 'inversify';
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution, SearchInWorkspaceCommands } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { EditorMode } from '../../editor-mode';
import { MenuModelRegistry } from '@theia/core';
@injectable()
export class SearchInWorkspaceFrontendContribution extends TheiaSearchInWorkspaceFrontendContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
@@ -21,4 +16,9 @@ export class SearchInWorkspaceFrontendContribution extends TheiaSearchInWorkspac
registry.unregisterMenuAction(SearchInWorkspaceCommands.OPEN_SIW_WIDGET);
}
registerKeybindings(keybindings: KeybindingRegistry): void {
super.registerKeybindings(keybindings);
keybindings.unregisterKeybinding(SearchInWorkspaceCommands.OPEN_SIW_WIDGET);
}
}

View File

@@ -7,9 +7,8 @@ import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { ConfigService } from '../../../common/protocol/config-service';
import { SketchesService } from '../../../common/protocol/sketches-service';
import { SketchesService, Sketch } from '../../../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';
@injectable()
@@ -85,7 +84,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
protected onCurrentWidgetChange({ newValue }: FocusTracker.IChangedArgs<Widget>): void {
if (newValue instanceof EditorWidget) {
const { uri } = newValue.editor;
if (uri.toString().endsWith('.ino')) {
if (Sketch.isSketchFile(uri.toString())) {
this.updateTitle();
} else {
const title = this.workspaceTitle;
@@ -98,9 +97,8 @@ export class WorkspaceService extends TheiaWorkspaceService {
}
protected formatTitle(title?: string): string {
const status = FrontendApplicationConfigProvider.get()['status'];
const version = this.version ? ` ${this.version}` : '';
const name = `${this.applicationName}${status ? ` ${status}` : ''} ${version}`;
const name = `${this.applicationName} ${version}`;
return title ? `${title} | ${name}` : name;
}

View File

@@ -6,13 +6,13 @@ import { ThemeConfig } from 'react-select/src/theme';
export class ArduinoSelect<T> extends Select<T> {
constructor(props: Readonly<Props<T>>) {
constructor(props: Readonly<Props<T, false>>) {
super(props);
}
render(): React.ReactNode {
const controlHeight = 27; // from `monitor.css` -> `.serial-monitor-container .head` (`height: 27px;`)
const styles: Styles = {
const styles: Styles<T, false> = {
control: styles => ({
...styles,
minWidth: 120,
@@ -51,6 +51,7 @@ export class ArduinoSelect<T> extends Select<T> {
const DropdownIndicator = () => <span className='fa fa-caret-down caret' />;
return <Select
{...this.props}
className='theia-select'
components={{ DropdownIndicator }}
theme={theme}
styles={styles}

View File

@@ -10,4 +10,8 @@ export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent>
async initializeLayout(): Promise<void> {
}
}
registerMenus(): void {
// NOOP
}
}

View File

@@ -5,6 +5,25 @@ import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
export namespace AvailablePorts {
export function groupByProtocol(availablePorts: AvailablePorts): { serial: AvailablePorts, network: AvailablePorts, unknown: AvailablePorts } {
const serial: AvailablePorts = {};
const network: AvailablePorts = {};
const unknown: AvailablePorts = {};
for (const key of Object.keys(availablePorts)) {
const [port, boards] = availablePorts[key];
const { protocol } = port;
if (protocol === 'serial') {
serial[key] = [port, boards];
} else if (protocol === 'network') {
network[key] = [port, boards];
} else {
unknown[key] = [port, boards];
}
}
return { serial, network, unknown };
}
}
export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;
@@ -267,12 +286,25 @@ export namespace BoardWithPackage {
}
export interface InstalledBoardWithPackage extends BoardWithPackage {
readonly fqbn: string;
}
export namespace InstalledBoardWithPackage {
export function is(boardWithPackage: BoardWithPackage): boardWithPackage is InstalledBoardWithPackage {
return !!boardWithPackage.fqbn;
}
}
export interface BoardDetails {
readonly fqbn: string;
readonly requiredTools: Tool[];
readonly configOptions: ConfigOption[];
readonly programmers: Programmer[];
readonly debuggingSupported: boolean;
readonly VID: string;
readonly PID: string;
}
export interface Tool {

View File

@@ -3,15 +3,100 @@ export const ConfigService = Symbol('ConfigService');
export interface ConfigService {
getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>>;
getConfiguration(): Promise<Config>;
getCliConfigFileUri(): Promise<string>;
getConfigurationFileSchemaUri(): Promise<string>;
setConfiguration(config: Config): Promise<void>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
}
export interface ProxySettings {
protocol: string;
hostname: string;
port: string;
username: string;
password: string;
}
export type Network = 'none' | ProxySettings;
export namespace Network {
export function Default(): Network {
return {
protocol: 'http',
hostname: '',
port: '',
username: '',
password: ''
}
}
export function parse(raw: string | undefined): Network {
if (!raw) {
return 'none';
}
try {
// Patter: PROTOCOL://USER:PASS@HOSTNAME:PORT/
const { protocol, hostname, password, username, port } = new URL(raw);
return {
protocol,
hostname,
password,
username,
port
};
} catch {
return 'none';
}
};
export function stringify(network: Network): string | undefined {
if (network === 'none') {
return undefined;
}
const { protocol, hostname, password, username, port } = network;
try {
const defaultUrl = new URL(`${protocol ? protocol : 'http'}://${hostname ? hostname : '_'}`);
return Object.assign(defaultUrl, { protocol, hostname, password, username, port }).toString();
} catch {
return undefined;
}
}
export function sameAs(left: Network, right: Network): boolean {
if (left === 'none') {
return right === 'none';
}
if (right === 'none') {
return false;
}
return left.hostname === right.hostname
&& left.password === right.password
&& left.protocol === right.protocol
&& left.username === right.username;
};
}
export interface Config {
readonly sketchDirUri: string;
readonly dataDirUri: string;
readonly downloadsDirUri: string;
readonly additionalUrls: string[];
readonly network: Network;
}
export namespace Config {
export function sameAs(left: Config, right: Config): boolean {
const leftUrls = left.additionalUrls.sort();
const rightUrls = right.additionalUrls.sort();
if (leftUrls.length !== rightUrls.length) {
return false;
}
for (let i = 0; i < leftUrls.length; i++) {
if (leftUrls[i] !== rightUrls[i]) {
return false;
}
}
return left.dataDirUri === right.dataDirUri
&& left.downloadsDirUri === right.downloadsDirUri
&& left.sketchDirUri === right.sketchDirUri
&& Network.sameAs(left.network, right.network);
}
}

View File

@@ -3,7 +3,7 @@ import { Programmer } from './boards-service';
export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService');
export interface CoreService {
compile(options: CoreService.Compile.Options): Promise<void>;
compile(options: CoreService.Compile.Options & Readonly<{ exportBinaries?: boolean }>): Promise<void>;
upload(options: CoreService.Upload.Options): Promise<void>;
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
@@ -13,9 +13,14 @@ export namespace CoreService {
export namespace Compile {
export interface Options {
/**
* `file` URI to the sketch folder.
*/
readonly sketchUri: string;
readonly fqbn?: string | undefined;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;
readonly sourceOverride: Record<string, string>;
}
}
@@ -23,6 +28,7 @@ export namespace CoreService {
export interface Options extends Compile.Options {
readonly port?: string | undefined;
readonly programmer?: Programmer | undefined;
readonly verify: boolean;
}
}
@@ -31,6 +37,8 @@ export namespace CoreService {
readonly fqbn?: string | undefined;
readonly port?: string | undefined;
readonly programmer?: Programmer | undefined;
readonly verbose: boolean;
readonly verify: boolean;
}
}

View File

@@ -69,4 +69,17 @@ export namespace LibraryPackage {
return left.name === right.name && left.author === right.author;
}
export function groupByLocation(packages: LibraryPackage[]): { user: LibraryPackage[], rest: LibraryPackage[] } {
const user: LibraryPackage[] = [];
const rest: LibraryPackage[] = [];
for (const pkg of packages) {
if (pkg.location === LibraryLocation.USER) {
user.push(pkg);
} else {
rest.push(pkg);
}
}
return { user, rest };
}
}

View File

@@ -1,7 +1,6 @@
import { LibraryPackage } from './library-service';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { BoardsPackage, AttachedBoardsChangeEvent } from './boards-service';
import { Config } from './config-service';
import { Sketch, Config, BoardsPackage, AttachedBoardsChangeEvent } from '../protocol';
export interface NotificationServiceClient {
notifyIndexUpdated(): void;
@@ -13,6 +12,7 @@ export interface NotificationServiceClient {
notifyLibraryInstalled(event: { item: LibraryPackage }): void;
notifyLibraryUninstalled(event: { item: LibraryPackage }): void;
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void;
}
export const NotificationServicePath = '/services/notification-service';

View File

@@ -1,5 +1,4 @@
export interface OutputMessage {
readonly name: string;
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
}

View File

@@ -5,9 +5,13 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { Sketch, SketchesService } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ConfigService } from './config-service';
import { DisposableCollection, Emitter } from '@theia/core';
import { FileChangeType } from '@theia/filesystem/lib/browser';
@injectable()
export class SketchesServiceClientImpl {
export class SketchesServiceClientImpl implements FrontendApplicationContribution {
@inject(FileService)
protected readonly fileService: FileService;
@@ -21,6 +25,57 @@ export class SketchesServiceClientImpl {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ConfigService)
protected readonly configService: ConfigService;
protected toDispose = new DisposableCollection();
protected sketches = new Map<string, Sketch>();
protected sketchbookDidChangeEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event;
onStart(): void {
this.configService.getConfiguration().then(({ sketchDirUri }) => {
this.sketchService.getSketches(sketchDirUri).then(sketches => {
const sketchbookUri = new URI(sketchDirUri);
for (const sketch of sketches) {
this.sketches.set(sketch.uri, sketch);
}
this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] }));
this.toDispose.push(this.fileService.onDidFilesChange(async event => {
for (const { type, resource } of event.changes) {
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
if (sketchbookUri.isEqualOrParent(resource)) {
if (Sketch.isSketchFile(resource)) {
if (type === FileChangeType.ADDED) {
try {
const toAdd = await this.sketchService.loadSketch(resource.parent.toString());
if (!this.sketches.has(toAdd.uri)) {
console.log(`New sketch '${toAdd.name}' was crated in sketchbook '${sketchDirUri}'.`);
this.sketches.set(toAdd.uri, toAdd);
this.fireSoon(toAdd, 'created');
}
} catch { }
} else if (type === FileChangeType.DELETED) {
const uri = resource.parent.toString();
const toDelete = this.sketches.get(uri);
if (toDelete) {
console.log(`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookUri}'.`);
this.sketches.delete(uri);
this.fireSoon(toDelete, 'removed');
}
}
}
}
}
}));
});
});
}
onStop(): void {
this.toDispose.dispose();
}
async currentSketch(): Promise<Sketch | undefined> {
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ resource }) => this.sketchService.getSketchFolder(resource.toString())))).filter(notEmpty);
if (!sketches.length) {
@@ -46,4 +101,31 @@ export class SketchesServiceClientImpl {
return undefined;
}
private fireSoonHandle?: number;
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
private fireSoon(sketch: Sketch, type: 'created' | 'removed'): void {
this.bufferedSketchbookEvents.push({ type, sketch });
if (typeof this.fireSoonHandle === 'number') {
window.clearTimeout(this.fireSoonHandle);
}
this.fireSoonHandle = window.setTimeout(() => {
const event: { created: Sketch[], removed: Sketch[] } = {
created: [],
removed: []
};
for (const { type, sketch } of this.bufferedSketchbookEvents) {
if (type === 'created') {
event.created.push(sketch);
} else {
event.removed.push(sketch);
}
}
this.sketchbookDidChangeEmitter.fire(event);
this.bufferedSketchbookEvents.length = 0;
}, 100);
}
}

View File

@@ -48,6 +48,21 @@ export interface SketchesService {
*/
getSketchFolder(uri: string): Promise<Sketch | undefined>;
/**
* Marks the sketch with the given URI as recently opened. It does nothing if the sketch is temp or not valid.
*/
markAsRecentlyOpened(uri: string): Promise<void>;
/**
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
*/
recentlyOpenedSketches(): Promise<Sketch[]>;
/**
* Archives the sketch, resolves to the archive URI.
*/
archive(sketch: Sketch, destinationUri: string): Promise<string>;
}
export interface Sketch {
@@ -56,6 +71,7 @@ export interface Sketch {
readonly mainFileUri: string; // `MainFile`
readonly otherSketchFileUris: string[]; // `OtherSketchFiles`
readonly additionalFileUris: string[]; // `AdditionalFiles`
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
}
export namespace Sketch {
export function is(arg: any): arg is Sketch {
@@ -64,12 +80,17 @@ export namespace Sketch {
export namespace Extensions {
export const MAIN = ['.ino', '.pde'];
export const SOURCE = ['.c', '.cpp', '.s'];
export const ADDITIONAL = ['.h', '.c', '.hpp', '.hh', '.cpp', '.s'];
export const ADDITIONAL = ['.h', '.c', '.hpp', '.hh', '.cpp', '.s', '.json'];
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
}
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(uri.toString()) !== -1;
}
export function isSketchFile(arg: string | URI): boolean {
if (arg instanceof URI) {
return isSketchFile(arg.toString());
}
return Extensions.MAIN.some(ext => arg.endsWith(ext));
}
}

View File

@@ -1,3 +1,7 @@
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};
export interface Index {
[key: string]: any;
}

View File

@@ -1,12 +1,18 @@
import { injectable } from 'inversify'
import { remote } from 'electron';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { ArduinoMenus } from '../../../browser/menu/arduino-menus';
import { CompositeMenuNode } from '@theia/core/lib/common/menu';
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { ArduinoMenus, PlaceholderMenuNode } from '../../../browser/menu/arduino-menus';
@injectable()
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
createMenuBar(): Electron.Menu {
this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
return super.createMenuBar();
}
protected acceleratorFor(keybinding: Keybinding): string {
// TODO: https://github.com/eclipse-theia/theia/issues/8207
return this.keybindingRegistry.resolveKeybinding(keybinding)
@@ -18,7 +24,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
protected createOSXMenu(): Electron.MenuItemConstructorOptions {
const { submenu } = super.createOSXMenu();
const label = 'Arduino Pro IDE';
const label = 'Arduino IDE';
if (!!submenu && !(submenu instanceof remote.Menu)) {
const [/* about */, /* settings */, ...rest] = submenu;
const about = this.fillMenuTemplate([], this.menuProvider.getMenu(ArduinoMenus.HELP__ABOUT_GROUP));
@@ -37,4 +43,15 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
return { label, submenu };
}
protected handleDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
if (menuNode instanceof PlaceholderMenuNode) {
return [{
label: menuNode.label,
enabled: false,
visible: true
}];
}
return [];
}
}

View File

@@ -1,15 +1,20 @@
import { ContainerModule } from 'inversify';
import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service';
import { ElectronMainApplication as TheiaElectronMainApplication } from '@theia/core/lib/electron-main/electron-main-application';
import { SplashService, splashServicePath } from '../electron-common/splash-service';
import { SplashServiceImpl } from './splash/splash-service-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMainApplication).toSelf().inSingletonScope();
rebind(TheiaElectronMainApplication).toService(ElectronMainApplication);
bind(ElectronMainWindowServiceImpl).toSelf().inSingletonScope();
rebind(ElectronMainWindowService).toService(ElectronMainWindowServiceImpl);
bind(SplashServiceImpl).toSelf().inSingletonScope();
bind(SplashService).toService(SplashServiceImpl);
bind(ElectronConnectionHandler).toDynamicValue(context =>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -13,7 +13,7 @@ import { SplashServiceImpl } from '../splash/splash-service-impl';
@injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication {
protected windows: BrowserWindow[] = [];
protected _windows: BrowserWindow[] = [];
@inject(SplashServiceImpl)
protected readonly splashService: SplashServiceImpl;
@@ -34,7 +34,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
async createWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions> = this.getDefaultBrowserWindowOptions()): Promise<BrowserWindow> {
const options = await asyncOptions;
let electronWindow: BrowserWindow | undefined;
if (this.windows.length) {
if (this._windows.length) {
electronWindow = new BrowserWindow(options);
} else {
const { bounds } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint());
@@ -63,14 +63,14 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
splashScreenOpts
}, this.splashService.onCloseRequested);
}
this.windows.push(electronWindow);
this._windows.push(electronWindow);
electronWindow.on('closed', () => {
if (electronWindow) {
const index = this.windows.indexOf(electronWindow);
const index = this._windows.indexOf(electronWindow);
if (index === -1) {
console.warn(`Could not dispose browser window: '${electronWindow.title}'.`);
} else {
this.windows.splice(index, 1);
this._windows.splice(index, 1);
electronWindow = undefined;
}
}
@@ -148,4 +148,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
}
}
get windows(): BrowserWindow[] {
return this._windows.slice();
}
}

View File

@@ -0,0 +1,24 @@
import { inject, injectable } from 'inversify';
import { NewWindowOptions } from '@theia/core/lib/browser/window/window-service';
import { ElectronMainWindowServiceImpl as TheiaElectronMainWindowService } from '@theia/core/lib/electron-main/electron-main-window-service-impl';
import { ElectronMainApplication } from './electron-main-application';
@injectable()
export class ElectronMainWindowServiceImpl extends TheiaElectronMainWindowService {
@inject(ElectronMainApplication)
protected readonly app: ElectronMainApplication;
openNewWindow(url: string, { external }: NewWindowOptions): undefined {
if (!external) {
const existing = this.app.windows.find(window => window.webContents.getURL() === url);
if (existing) {
existing.focus();
return;
}
}
return super.openNewWindow(url, { external });
}
}

View File

@@ -1,6 +1,3 @@
import * as fs from 'fs';
import * as os from 'os';
import { join } from 'path';
import { ContainerModule } from 'inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import { ILogger } from '@theia/core/lib/common/logger';
@@ -24,9 +21,6 @@ import { MonitorServiceImpl } from './monitor/monitor-service-impl';
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
import { MonitorClientProvider } from './monitor/monitor-client-provider';
import { ConfigServiceImpl } from './config-service-impl';
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
import { ConfigFileValidator } from './config-file-validator';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext';
@@ -46,7 +40,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaBackendApplication).toService(BackendApplication);
// Shared config service
bind(ConfigFileValidator).toSelf().inSingletonScope();
bind(ConfigServiceImpl).toSelf().inSingletonScope();
bind(ConfigService).toService(ConfigServiceImpl);
// Note: The config service must start earlier than the daemon, hence the binding order of the BA contribution does matter.
@@ -78,7 +71,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bindBackendService(LibraryServicePath, LibraryService);
}));
// Shred sketches service
// Shared sketches service
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
@@ -111,9 +104,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(EnvVariablesServer).toSelf().inSingletonScope();
rebind(TheiaEnvVariablesServer).toService(EnvVariablesServer);
bind(HostedPluginReader).toSelf().inSingletonScope();
rebind(TheiaHostedPluginReader).toService(HostedPluginReader);
// #endregion Theia customizations
// Monitor client provider per connected frontend.
@@ -128,21 +118,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
});
}));
// Set up cpp extension
if (!process.env.CPP_CLANGD_COMMAND) {
const segments = ['..', '..', 'build'];
if (os.platform() === 'win32') {
segments.push('clangd.exe');
} else {
segments.push('bin');
segments.push('clangd');
}
const clangdCommand = join(__dirname, ...segments);
if (fs.existsSync(clangdCommand)) {
process.env.CPP_CLANGD_COMMAND = clangdCommand;
}
}
// File-system extension for mapping paths to URIs
bind(NodeFileSystemExt).toSelf().inSingletonScope();
bind(FileSystemExt).toService(NodeFileSystemExt);

View File

@@ -1,5 +1,6 @@
import { injectable, inject, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { notEmpty } from '@theia/core/lib/common/objects';
import {
BoardsService,
Installable,
@@ -128,12 +129,22 @@ export class BoardsServiceImpl implements BoardsService {
platform: p.getPlatform()
});
let VID = 'N/A';
let PID = 'N/A';
const usbId = detailsResp.getIdentificationPrefList().map(item => item.getUsbid()).find(notEmpty);
if (usbId) {
VID = usbId.getVid();
PID = usbId.getPid();
}
return {
fqbn,
requiredTools,
configOptions,
programmers,
debuggingSupported
debuggingSupported,
VID,
PID
};
}
@@ -260,7 +271,7 @@ export class BoardsServiceImpl implements BoardsService {
resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress();
if (prog && prog.getFile()) {
this.outputService.append({ name: 'board download', chunk: `downloading ${prog.getFile()}\n` });
this.outputService.append({ chunk: `downloading ${prog.getFile()}\n` });
}
});
await new Promise<void>((resolve, reject) => {
@@ -291,7 +302,7 @@ export class BoardsServiceImpl implements BoardsService {
const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResp) => {
if (!logged) {
this.outputService.append({ name: 'board uninstall', chunk: `uninstalling ${item.id}\n` });
this.outputService.append({ chunk: `uninstalling ${item.id}\n` });
logged = true;
}
})

View File

@@ -1,9 +1,6 @@
import { join } from 'path';
import { RecursivePartial } from '@theia/core/lib/common/types';
export const CLI_CONFIG = 'arduino-cli.yaml';
export const CLI_CONFIG_SCHEMA = 'arduino-cli.schema.json';
export const CLI_CONFIG_SCHEMA_PATH = join(__dirname, '..', '..', 'data', 'cli', 'schema', CLI_CONFIG_SCHEMA);
export interface BoardManager {
readonly additional_urls: Array<string>;
@@ -95,11 +92,16 @@ export namespace Logging {
}
export interface Network {
proxy?: string;
}
// Arduino CLI config scheme
export interface CliConfig {
board_manager?: RecursivePartial<BoardManager>;
directories?: RecursivePartial<Directories>;
logging?: RecursivePartial<Logging>;
network?: RecursivePartial<Network>;
}
// Bare minimum required CLI config.

View File

@@ -1175,7 +1175,7 @@ libraryInstall: {
responseSerialize: serialize_cc_arduino_cli_commands_LibraryInstallResp,
responseDeserialize: deserialize_cc_arduino_cli_commands_LibraryInstallResp,
},
// Install a library from a Zip File
// Install a library from a Zip File
zipLibraryInstall: {
path: '/cc.arduino.cli.commands.ArduinoCore/ZipLibraryInstall',
requestStream: false,

View File

@@ -512,6 +512,11 @@ export class LoadSketchResp extends jspb.Message {
setAdditionalFilesList(value: Array<string>): LoadSketchResp;
addAdditionalFiles(value: string, index?: number): string;
clearRootFolderFilesList(): void;
getRootFolderFilesList(): Array<string>;
setRootFolderFilesList(value: Array<string>): LoadSketchResp;
addRootFolderFiles(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): LoadSketchResp.AsObject;
@@ -529,6 +534,7 @@ export namespace LoadSketchResp {
locationPath: string,
otherSketchFilesList: Array<string>,
additionalFilesList: Array<string>,
rootFolderFilesList: Array<string>,
}
}

View File

@@ -3639,7 +3639,7 @@ proto.cc.arduino.cli.commands.LoadSketchReq.prototype.setSketchPath = function(v
* @private {!Array<number>}
* @const
*/
proto.cc.arduino.cli.commands.LoadSketchResp.repeatedFields_ = [3,4];
proto.cc.arduino.cli.commands.LoadSketchResp.repeatedFields_ = [3,4,5];
@@ -3675,7 +3675,8 @@ proto.cc.arduino.cli.commands.LoadSketchResp.toObject = function(includeInstance
mainFile: jspb.Message.getFieldWithDefault(msg, 1, ""),
locationPath: jspb.Message.getFieldWithDefault(msg, 2, ""),
otherSketchFilesList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f,
additionalFilesList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f
additionalFilesList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f,
rootFolderFilesList: (f = jspb.Message.getRepeatedField(msg, 5)) == null ? undefined : f
};
if (includeInstance) {
@@ -3728,6 +3729,10 @@ proto.cc.arduino.cli.commands.LoadSketchResp.deserializeBinaryFromReader = funct
var value = /** @type {string} */ (reader.readString());
msg.addAdditionalFiles(value);
break;
case 5:
var value = /** @type {string} */ (reader.readString());
msg.addRootFolderFiles(value);
break;
default:
reader.skipField();
break;
@@ -3785,6 +3790,13 @@ proto.cc.arduino.cli.commands.LoadSketchResp.serializeBinaryToWriter = function(
f
);
}
f = message.getRootFolderFilesList();
if (f.length > 0) {
writer.writeRepeatedString(
5,
f
);
}
};
@@ -3898,6 +3910,43 @@ proto.cc.arduino.cli.commands.LoadSketchResp.prototype.clearAdditionalFilesList
};
/**
* repeated string root_folder_files = 5;
* @return {!Array<string>}
*/
proto.cc.arduino.cli.commands.LoadSketchResp.prototype.getRootFolderFilesList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 5));
};
/**
* @param {!Array<string>} value
* @return {!proto.cc.arduino.cli.commands.LoadSketchResp} returns this
*/
proto.cc.arduino.cli.commands.LoadSketchResp.prototype.setRootFolderFilesList = function(value) {
return jspb.Message.setField(this, 5, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.cc.arduino.cli.commands.LoadSketchResp} returns this
*/
proto.cc.arduino.cli.commands.LoadSketchResp.prototype.addRootFolderFiles = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 5, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.cc.arduino.cli.commands.LoadSketchResp} returns this
*/
proto.cc.arduino.cli.commands.LoadSketchResp.prototype.clearRootFolderFilesList = function() {
return this.setRootFolderFilesList([]);
};

View File

@@ -5,6 +5,7 @@
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as google_protobuf_wrappers_pb from "google-protobuf/google/protobuf/wrappers_pb";
import * as commands_common_pb from "../commands/common_pb";
import * as commands_lib_pb from "../commands/lib_pb";
@@ -67,8 +68,18 @@ export class CompileReq extends jspb.Message {
getClean(): boolean;
setClean(value: boolean): CompileReq;
getExportBinaries(): boolean;
setExportBinaries(value: boolean): CompileReq;
getCreateCompilationDatabaseOnly(): boolean;
setCreateCompilationDatabaseOnly(value: boolean): CompileReq;
getSourceOverrideMap(): jspb.Map<string, string>;
clearSourceOverrideMap(): void;
hasExportBinaries(): boolean;
clearExportBinaries(): void;
getExportBinaries(): google_protobuf_wrappers_pb.BoolValue | undefined;
setExportBinaries(value?: google_protobuf_wrappers_pb.BoolValue): CompileReq;
serializeBinary(): Uint8Array;
@@ -100,7 +111,10 @@ export namespace CompileReq {
optimizefordebug: boolean,
exportDir: string,
clean: boolean,
exportBinaries: boolean,
createCompilationDatabaseOnly: boolean,
sourceOverrideMap: Array<[string, string]>,
exportBinaries?: google_protobuf_wrappers_pb.BoolValue.AsObject,
}
}

View File

@@ -14,6 +14,8 @@ var jspb = require('google-protobuf');
var goog = jspb;
var global = Function('return this')();
var google_protobuf_wrappers_pb = require('google-protobuf/google/protobuf/wrappers_pb.js');
goog.object.extend(proto, google_protobuf_wrappers_pb);
var commands_common_pb = require('../commands/common_pb.js');
goog.object.extend(proto, commands_common_pb);
var commands_lib_pb = require('../commands/lib_pb.js');
@@ -140,7 +142,9 @@ proto.cc.arduino.cli.commands.CompileReq.toObject = function(includeInstance, ms
optimizefordebug: jspb.Message.getBooleanFieldWithDefault(msg, 16, false),
exportDir: jspb.Message.getFieldWithDefault(msg, 18, ""),
clean: jspb.Message.getBooleanFieldWithDefault(msg, 19, false),
exportBinaries: jspb.Message.getBooleanFieldWithDefault(msg, 20, false)
createCompilationDatabaseOnly: jspb.Message.getBooleanFieldWithDefault(msg, 21, false),
sourceOverrideMap: (f = msg.getSourceOverrideMap()) ? f.toObject(includeInstance, undefined) : [],
exportBinaries: (f = msg.getExportBinaries()) && google_protobuf_wrappers_pb.BoolValue.toObject(includeInstance, f)
};
if (includeInstance) {
@@ -246,8 +250,19 @@ proto.cc.arduino.cli.commands.CompileReq.deserializeBinaryFromReader = function(
var value = /** @type {boolean} */ (reader.readBool());
msg.setClean(value);
break;
case 20:
case 21:
var value = /** @type {boolean} */ (reader.readBool());
msg.setCreateCompilationDatabaseOnly(value);
break;
case 22:
var value = msg.getSourceOverrideMap();
reader.readMessage(value, function(message, reader) {
jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString, null, "", "");
});
break;
case 23:
var value = new google_protobuf_wrappers_pb.BoolValue;
reader.readMessage(value,google_protobuf_wrappers_pb.BoolValue.deserializeBinaryFromReader);
msg.setExportBinaries(value);
break;
default:
@@ -399,13 +414,25 @@ proto.cc.arduino.cli.commands.CompileReq.serializeBinaryToWriter = function(mess
f
);
}
f = message.getExportBinaries();
f = message.getCreateCompilationDatabaseOnly();
if (f) {
writer.writeBool(
20,
21,
f
);
}
f = message.getSourceOverrideMap(true);
if (f && f.getLength() > 0) {
f.serializeBinary(22, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString);
}
f = message.getExportBinaries();
if (f != null) {
writer.writeMessage(
23,
f,
google_protobuf_wrappers_pb.BoolValue.serializeBinaryToWriter
);
}
};
@@ -773,11 +800,11 @@ proto.cc.arduino.cli.commands.CompileReq.prototype.setClean = function(value) {
/**
* optional bool export_binaries = 20;
* optional bool create_compilation_database_only = 21;
* @return {boolean}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.getExportBinaries = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 20, false));
proto.cc.arduino.cli.commands.CompileReq.prototype.getCreateCompilationDatabaseOnly = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 21, false));
};
@@ -785,8 +812,67 @@ proto.cc.arduino.cli.commands.CompileReq.prototype.getExportBinaries = function(
* @param {boolean} value
* @return {!proto.cc.arduino.cli.commands.CompileReq} returns this
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.setCreateCompilationDatabaseOnly = function(value) {
return jspb.Message.setProto3BooleanField(this, 21, value);
};
/**
* map<string, string> source_override = 22;
* @param {boolean=} opt_noLazyCreate Do not create the map if
* empty, instead returning `undefined`
* @return {!jspb.Map<string,string>}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.getSourceOverrideMap = function(opt_noLazyCreate) {
return /** @type {!jspb.Map<string,string>} */ (
jspb.Message.getMapField(this, 22, opt_noLazyCreate,
null));
};
/**
* Clears values from the map. The map will be non-null.
* @return {!proto.cc.arduino.cli.commands.CompileReq} returns this
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.clearSourceOverrideMap = function() {
this.getSourceOverrideMap().clear();
return this;};
/**
* optional google.protobuf.BoolValue export_binaries = 23;
* @return {?proto.google.protobuf.BoolValue}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.getExportBinaries = function() {
return /** @type{?proto.google.protobuf.BoolValue} */ (
jspb.Message.getWrapperField(this, google_protobuf_wrappers_pb.BoolValue, 23));
};
/**
* @param {?proto.google.protobuf.BoolValue|undefined} value
* @return {!proto.cc.arduino.cli.commands.CompileReq} returns this
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.setExportBinaries = function(value) {
return jspb.Message.setProto3BooleanField(this, 20, value);
return jspb.Message.setWrapperField(this, 23, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.CompileReq} returns this
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.clearExportBinaries = function() {
return this.setExportBinaries(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.hasExportBinaries = function() {
return jspb.Message.getField(this, 23) != null;
};

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