mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-09-30 07:08:33 +00:00
Compare commits
226 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f1bffaab2d | ||
![]() |
3191a09562 | ||
![]() |
40905a058c | ||
![]() |
e7b1a27401 | ||
![]() |
4d5a046aa8 | ||
![]() |
c024a8d3d1 | ||
![]() |
7696e2c4c9 | ||
![]() |
1acf13c397 | ||
![]() |
cff2c95684 | ||
![]() |
1a531db0b7 | ||
![]() |
2e00e2db35 | ||
![]() |
ca1b288706 | ||
![]() |
41eeb337f9 | ||
![]() |
39b2e49edb | ||
![]() |
138afbf7fd | ||
![]() |
01e42dafde | ||
![]() |
2831acc5b5 | ||
![]() |
acbb7d32b2 | ||
![]() |
781747fe80 | ||
![]() |
874c3efa2c | ||
![]() |
7b364ebe60 | ||
![]() |
a96449f557 | ||
![]() |
c78e474790 | ||
![]() |
30136b0ef2 | ||
![]() |
53b06aef67 | ||
![]() |
6535c70686 | ||
![]() |
6ff58ebe7c | ||
![]() |
7068b9b1d3 | ||
![]() |
e755a1cd7e | ||
![]() |
def93ea32f | ||
![]() |
5f5193932f | ||
![]() |
f26dae185b | ||
![]() |
fbebfc7cca | ||
![]() |
c3eb3e4622 | ||
![]() |
30421f0de4 | ||
![]() |
daa25794ef | ||
![]() |
ec8df37c2d | ||
![]() |
cb24571eeb | ||
![]() |
2f8e28b296 | ||
![]() |
524fbbdf40 | ||
![]() |
7a37aa2e2f | ||
![]() |
56ff86629c | ||
![]() |
1c9fcd0cdf | ||
![]() |
b5d7c3b45d | ||
![]() |
525e688d70 | ||
![]() |
d7f4d0c18e | ||
![]() |
6ae7404092 | ||
![]() |
6b1b9c0524 | ||
![]() |
717db95c90 | ||
![]() |
7536c3a485 | ||
![]() |
ec7df884ee | ||
![]() |
24b6d84d27 | ||
![]() |
4435696949 | ||
![]() |
6aa3ff8044 | ||
![]() |
4b44113f2c | ||
![]() |
c6ad0f582a | ||
![]() |
4b8b468e53 | ||
![]() |
ba319b23d9 | ||
![]() |
70278fed6f | ||
![]() |
d5c7d3a8ed | ||
![]() |
0ddfa1cfdc | ||
![]() |
d13a43668e | ||
![]() |
5f515f0597 | ||
![]() |
1078f6fa96 | ||
![]() |
5ac7fe1446 | ||
![]() |
0363041b39 | ||
![]() |
4c4043b73c | ||
![]() |
fc0fe2196f | ||
![]() |
82a3b01c0b | ||
![]() |
7754cf6334 | ||
![]() |
cb1c16fb26 | ||
![]() |
d07763a854 | ||
![]() |
f605994d7d | ||
![]() |
e72e794266 | ||
![]() |
ee327eff18 | ||
![]() |
960d8dba2e | ||
![]() |
17de5e1a35 | ||
![]() |
af21f07f84 | ||
![]() |
2e729fc29e | ||
![]() |
bc4c3e04f5 | ||
![]() |
aa2bed8d39 | ||
![]() |
3806d56e94 | ||
![]() |
f0015e3fb4 | ||
![]() |
650230a571 | ||
![]() |
aadd403cdb | ||
![]() |
6da702abd5 | ||
![]() |
4cad068e8d | ||
![]() |
bb705620c0 | ||
![]() |
e36e42910c | ||
![]() |
a49ba05d5e | ||
![]() |
b87782ed05 | ||
![]() |
e4e6217153 | ||
![]() |
bcc715e11c | ||
![]() |
7f0bdfd45e | ||
![]() |
efd1be3e85 | ||
![]() |
eb2161cc6e | ||
![]() |
9ee5fc097c | ||
![]() |
f375202c5d | ||
![]() |
fc09dc3613 | ||
![]() |
3d55aaa875 | ||
![]() |
fc866464ad | ||
![]() |
ada0f4c7ed | ||
![]() |
111ba7fef3 | ||
![]() |
1f7e06f990 | ||
![]() |
f503ef0fcf | ||
![]() |
43abbf6fb8 | ||
![]() |
5aae800a4e | ||
![]() |
a2b3d9b314 | ||
![]() |
06c3015158 | ||
![]() |
8ab70f48f8 | ||
![]() |
528f4150d3 | ||
![]() |
696048b5d9 | ||
![]() |
722f8d7534 | ||
![]() |
4a9a975dca | ||
![]() |
6661f661c7 | ||
![]() |
a54c860dbb | ||
![]() |
84016f4e1e | ||
![]() |
fd2b03e286 | ||
![]() |
ce58590c0e | ||
![]() |
1b8c510f33 | ||
![]() |
5d8fbcd437 | ||
![]() |
747f5d6a14 | ||
![]() |
d9f4adfb78 | ||
![]() |
d51bf9fb40 | ||
![]() |
f4097b5b24 | ||
![]() |
491e0cb6d0 | ||
![]() |
e95f00466f | ||
![]() |
e1f74d94a7 | ||
![]() |
01c21266e5 | ||
![]() |
8aa08cbf6e | ||
![]() |
fdc5814e66 | ||
![]() |
87b383f57e | ||
![]() |
cc76f2bbc8 | ||
![]() |
deea43008d | ||
![]() |
42d2285fa8 | ||
![]() |
8b27cd7bb9 | ||
![]() |
230bacfd01 | ||
![]() |
e1d86d0bda | ||
![]() |
e77c9721cd | ||
![]() |
07692fe368 | ||
![]() |
3465407b5a | ||
![]() |
89c348baac | ||
![]() |
824d1df4bd | ||
![]() |
445ffedf02 | ||
![]() |
bbf880d187 | ||
![]() |
12a13b869c | ||
![]() |
271b06d98a | ||
![]() |
587eb87416 | ||
![]() |
2f8ac44152 | ||
![]() |
47fb5105c5 | ||
![]() |
673c61506e | ||
![]() |
79a5d2272f | ||
![]() |
537291b494 | ||
![]() |
23450ce863 | ||
![]() |
c9dfc68911 | ||
![]() |
0ee9d16b40 | ||
![]() |
2f3fe27da3 | ||
![]() |
dfed2350bd | ||
![]() |
a74b159366 | ||
![]() |
5788ca3761 | ||
![]() |
5d23cb5270 | ||
![]() |
95dfd3920c | ||
![]() |
89faa9d45c | ||
![]() |
42d5a08c3b | ||
![]() |
88631a318b | ||
![]() |
540a6215e4 | ||
![]() |
c0cadf2b9b | ||
![]() |
939a905a6b | ||
![]() |
33af9b6005 | ||
![]() |
e0d4634f32 | ||
![]() |
83e074e895 | ||
![]() |
7873e492d4 | ||
![]() |
e8c3abd2ec | ||
![]() |
3a6b2f2bc8 | ||
![]() |
cfde197198 | ||
![]() |
20f44fe072 | ||
![]() |
5abfc2c1aa | ||
![]() |
b6df6b57a1 | ||
![]() |
218aaf64c7 | ||
![]() |
a0bbfe0b5c | ||
![]() |
4326c5f9f2 | ||
![]() |
d29141aff6 | ||
![]() |
55ce04fe3f | ||
![]() |
2eab9a0792 | ||
![]() |
88dcaba3ae | ||
![]() |
f392ce752d | ||
![]() |
2bd9eef146 | ||
![]() |
20bd7bcd64 | ||
![]() |
3082b3d643 | ||
![]() |
18ca84d682 | ||
![]() |
16cb2cf88b | ||
![]() |
e7a134a181 | ||
![]() |
0942ef6450 | ||
![]() |
68f95ae5b4 | ||
![]() |
976f858c7c | ||
![]() |
ee196bfc3a | ||
![]() |
8e8a8ac945 | ||
![]() |
2f93af4faa | ||
![]() |
66d29d6155 | ||
![]() |
45f06718b4 | ||
![]() |
08e3731493 | ||
![]() |
bf1351ffdc | ||
![]() |
8da8f2a50e | ||
![]() |
5a353ffaa5 | ||
![]() |
951884045c | ||
![]() |
6bb852c613 | ||
![]() |
d9a866579c | ||
![]() |
ecce237c6c | ||
![]() |
ae6aec27db | ||
![]() |
f6f6cb20de | ||
![]() |
02d1f349ad | ||
![]() |
f71a266fe3 | ||
![]() |
d8a742e440 | ||
![]() |
f1fca4d5b3 | ||
![]() |
4732a1d68b | ||
![]() |
6ce4143d49 | ||
![]() |
d54a69935e | ||
![]() |
12f2aa35ff | ||
![]() |
5c16f8d6c9 | ||
![]() |
807b2ad424 | ||
![]() |
c7bf98dfa3 | ||
![]() |
b2d53b1a35 | ||
![]() |
a9bbf47a59 | ||
![]() |
3974f2d8a1 | ||
![]() |
2a2238bb82 | ||
![]() |
44504ab916 |
179
.github/workflows/build.yml
vendored
Normal file
179
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
name: Arduino Pro IDE
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-latest
|
||||
- os: ubuntu-latest
|
||||
- os: macos-latest
|
||||
# - os: rsora-rpi-arm # self-hosted armhf
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.14.1'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Python 2.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '2.7'
|
||||
|
||||
- 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_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
# See: https://www.electron.build/code-signing
|
||||
if [ "${{ runner.OS }}" = "macOS" ]; then
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
|
||||
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
|
||||
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
|
||||
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
|
||||
|
||||
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
|
||||
|
||||
elif [ "${{ runner.OS }}" = "Windows" ]; then
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
|
||||
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
|
||||
|
||||
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
|
||||
fi
|
||||
|
||||
yarn --cwd ./electron/packager/
|
||||
yarn --cwd ./electron/packager/ package
|
||||
|
||||
- name: Upload [GitHub Actions]
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: electron/build/dist/build-artifacts/
|
||||
|
||||
changelog:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
BODY: ${{ steps.changelog.outputs.BODY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # To fetch all history for all branches and tags.
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
env:
|
||||
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
export LATEST_TAG=$(git describe --abbrev=0)
|
||||
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
|
||||
if [ "$IS_RELEASE" = true ]; then
|
||||
export BODY=$(echo -e "$GIT_LOG")
|
||||
else
|
||||
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-pro-ide/releases/tag/$LATEST_TAG)")
|
||||
if [ -z "$GIT_LOG" ]; then
|
||||
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
|
||||
else
|
||||
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
|
||||
fi
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
|
||||
echo "$BODY" > CHANGELOG.txt
|
||||
|
||||
- name: Upload Changelog [GitHub Actions]
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master')
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: CHANGELOG.txt
|
||||
|
||||
publish:
|
||||
needs: changelog
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build-artifacts
|
||||
|
||||
- name: Publish Nightly [S3]
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "build-artifacts/*"
|
||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
||||
PLUGIN_TARGET: "/arduino-pro-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 }}
|
||||
|
||||
release:
|
||||
needs: changelog
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build-artifacts
|
||||
|
||||
- name: Get Tag
|
||||
id: tag_name
|
||||
run: |
|
||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||
|
||||
- name: Publish Release [GitHub]
|
||||
uses: svenstaro/upload-release-action@2.2.0
|
||||
with:
|
||||
repo_token: ${{ secrets.RELEASE_TOKEN }}
|
||||
repo_name: arduino/arduino-pro-ide
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
file: build-artifacts/*
|
||||
tag: ${{ github.ref }}
|
||||
file_glob: true
|
||||
body: ${{ needs.changelog.outputs.BODY }}
|
||||
|
||||
- name: Publish Release [S3]
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "build-artifacts/*"
|
||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
||||
PLUGIN_TARGET: "/arduino-pro-ide"
|
||||
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
122
.github/workflows/check-certificates.yml
vendored
Normal file
122
.github/workflows/check-certificates.yml
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
name: Check for issues with signing certificates
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every 10 hours
|
||||
- cron: "0 */10 * * *"
|
||||
# workflow_dispatch event allows the workflow to be triggered manually.
|
||||
# This could be used to run an immediate check after updating certificate secrets.
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Begin notifications when there are less than this many days remaining before expiration
|
||||
EXPIRATION_WARNING_PERIOD: 30
|
||||
|
||||
jobs:
|
||||
check-certificates:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
certificate:
|
||||
- identifier: macOS signing certificate # Text used to identify the certificate in notifications
|
||||
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate
|
||||
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password
|
||||
- identifier: Windows signing certificate
|
||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
|
||||
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
|
||||
|
||||
steps:
|
||||
- name: Set certificate path environment variable
|
||||
run: |
|
||||
# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
|
||||
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Decode certificate
|
||||
env:
|
||||
CERTIFICATE: ${{ secrets[matrix.certificate.certificate-secret] }}
|
||||
run: |
|
||||
echo "${{ env.CERTIFICATE }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"
|
||||
|
||||
- name: Verify certificate
|
||||
env:
|
||||
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
|
||||
run: |
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-noout -passin env:CERTIFICATE_PASSWORD
|
||||
) || (
|
||||
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
|
||||
exit 1
|
||||
)
|
||||
|
||||
# See: https://github.com/rtCamp/action-slack-notify
|
||||
- name: Slack notification of certificate verification failure
|
||||
if: failure()
|
||||
uses: rtCamp/action-slack-notify@v2.1.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
||||
SLACK_MESSAGE: |
|
||||
:warning::warning::warning::warning:
|
||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
|
||||
:warning::warning::warning::warning:
|
||||
SLACK_COLOR: danger
|
||||
MSG_MINIMAL: true
|
||||
|
||||
- name: Get days remaining before certificate expiration date
|
||||
env:
|
||||
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
|
||||
id: get-days-before-expiration
|
||||
run: |
|
||||
EXPIRATION_DATE="$(
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-clcerts \
|
||||
-nodes \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) | (
|
||||
openssl x509 \
|
||||
-noout \
|
||||
-enddate
|
||||
) | (
|
||||
grep \
|
||||
--max-count=1 \
|
||||
--only-matching \
|
||||
--perl-regexp \
|
||||
'notAfter=(\K.*)'
|
||||
)
|
||||
)"
|
||||
|
||||
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
|
||||
|
||||
# Display the expiration information in the log
|
||||
echo "Certificate expiration date: $EXPIRATION_DATE"
|
||||
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
|
||||
|
||||
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
|
||||
|
||||
- name: Check if expiration notification period has been reached
|
||||
id: check-expiration
|
||||
run: |
|
||||
if [[ ${{ steps.get-days-before-expiration.outputs.days }} -lt ${{ env.EXPIRATION_WARNING_PERIOD }} ]]; then
|
||||
echo "::error::${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Slack notification of pending certificate expiration
|
||||
# Don't send spurious expiration notification if verification fails
|
||||
if: failure() && steps.check-expiration.outcome == 'failure'
|
||||
uses: rtCamp/action-slack-notify@v2.1.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
||||
SLACK_MESSAGE: |
|
||||
:warning::warning::warning::warning:
|
||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
|
||||
:warning::warning::warning::warning:
|
||||
SLACK_COLOR: danger
|
||||
MSG_MINIMAL: true
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -4,14 +4,15 @@ node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
*webpack.config.js
|
||||
.DS_Store
|
||||
/workspace/static
|
||||
.DS_Store
|
||||
# switching from `electron` to `browser` in dev mode.
|
||||
.browser_modules
|
||||
# LS logs
|
||||
inols*.log
|
||||
yarn-error.log
|
||||
yarn*.log
|
||||
# For the VS Code extensions used by Theia.
|
||||
plugins
|
||||
# the config files for the CLI
|
||||
arduino-ide-extension/data/cli/config
|
||||
|
30
.vscode/launch.json
vendored
30
.vscode/launch.json
vendored
@@ -13,7 +13,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Electron Packager",
|
||||
"name": "Electron Packager",
|
||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||
"cwd": "${workspaceFolder}/electron/packager"
|
||||
},
|
||||
@@ -36,7 +36,8 @@
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install"
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:plugins"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -61,7 +62,8 @@
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install"
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:plugins"
|
||||
],
|
||||
"windows": {
|
||||
"env": {
|
||||
@@ -85,24 +87,20 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Browser - Debug CLI daemon)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"protocol": "inspector",
|
||||
"name": "Run Test [current]",
|
||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install",
|
||||
"--debug-cli=true"
|
||||
"--require",
|
||||
"reflect-metadata/Reflect",
|
||||
"--no-timeouts",
|
||||
"--colors",
|
||||
"**/${fileBasenameNoExtension}.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
|
124
.vscode/tasks.json
vendored
124
.vscode/tasks.json
vendored
@@ -1,48 +1,78 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Arduino Editor - Start Browser Example",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Editor - Watch Theia Extension",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./arduino-ide-extension watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Editor - Watch Browser Example",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Editor - Watch All",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino Editor - Watch Theia Extension",
|
||||
"Arduino Editor - Watch Browser Example"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Arduino Pro IDE - Rebuild Electron App",
|
||||
"type": "shell",
|
||||
"command": "yarn rebuild:browser && yarn rebuild:electron",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Pro IDE - Start Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Pro IDE - Watch IDE Extension",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./arduino-ide-extension watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
}
|
||||
{
|
||||
"label": "Arduino Pro IDE - Watch Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Pro IDE - Watch Electron App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./electron-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino Pro IDE - Watch All [Browser]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino Pro IDE - Watch IDE Extension",
|
||||
"Arduino Pro IDE - Watch Browser App"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Arduino Pro IDE - Watch All [Electron]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino Pro IDE - Watch IDE Extension",
|
||||
"Arduino Pro IDE - Watch Electron App"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
147
README.md
147
README.md
@@ -1,50 +1,133 @@
|
||||
# Arduino Pro IDE
|
||||
|
||||
[](https://dev.azure.com/typefox/Arduino/_build/latest?definitionId=4&branchName=master)
|
||||
[](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22)
|
||||
|
||||
> **Beware:** This is very much work-in-progress. Things can and probably will be broken, even on master.
|
||||
### Download
|
||||
|
||||
This repo contains a proof-of-concept for an Arduino IDE based on Theia.
|
||||
It's built on top of a [fork of the arduino-cli](https://github.com/typefox/arduino-cli/tree/daemon) that sports a gRPC interface.
|
||||
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.
|
||||
|
||||
## How to try (offline)
|
||||
#### Latest version
|
||||
|
||||
### Prerequisites
|
||||
You should be able to build Theia locally. The requirements are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites).
|
||||
Platform | 32 bit | 64 bit |
|
||||
--------- | ------------------------ | ------------------------------------------------------------------------------ |
|
||||
Linux | | [Linux 64 bit] |
|
||||
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] |
|
||||
|
||||
```
|
||||
git clone https://github.com/bcmi-labs/arduino-editor
|
||||
cd arduino-editor
|
||||
[🚧 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
|
||||
|
||||
#### Previous versions
|
||||
|
||||
These are available from the [GitHub releases page](https://github.com/arduino/arduino-pro-ide/releases).
|
||||
|
||||
#### Nightly builds
|
||||
|
||||
These builds are generated every day at 03:00 GMT from the `master` branch and
|
||||
should be considered unstable. In order to get the latest nightly build
|
||||
available for the supported platform, use the following links:
|
||||
|
||||
Platform | 32 bit | 64 bit |
|
||||
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
Linux | | [Nightly Linux 64 bit] |
|
||||
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||
Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
||||
macOS | | [Nightly macOS 64 bit] |
|
||||
|
||||
[🚧 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
|
||||
|
||||
> These links return an HTTP `302: Found` response, redirecting to latest
|
||||
generated builds by replacing `latest` with the latest available build
|
||||
date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
||||
replaced with `20190806` )
|
||||
|
||||
### Build from source
|
||||
|
||||
If you’re 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.
|
||||
|
||||
### Build
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Rebuild the native dependencies
|
||||
```sh
|
||||
yarn rebuild:electron
|
||||
yarn --cwd electron-app start
|
||||
```
|
||||
|
||||
If you want to switch back to the browser-based example, execute the following in the repository root
|
||||
```
|
||||
yarn rebuild:browser
|
||||
```
|
||||
Then you can start the browser example again:
|
||||
```
|
||||
yarn --cwd browser-app start
|
||||
### Start
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
Click [here](./arduino-ide-extension/README.md) for more details on various IDE services, and the Arduino Pro IDE implementation in general.
|
||||
### CI
|
||||
|
||||
This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22).
|
||||
|
||||
## Arduino Pro IDE Electron Application
|
||||
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
|
||||
- _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.
|
||||
- _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
|
||||
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
|
||||
```
|
||||
- Push it to the remote:
|
||||
```sh
|
||||
git push origin 1.2.3
|
||||
```
|
||||
|
||||
Currently, we provide the Arduino Pro IDE for the following platforms:
|
||||
- Windows,
|
||||
- macOS, and
|
||||
- Linux.
|
||||
|
||||
You can download the latest release applications fom [here](https://github.com/bcmi-labs/arduino-editor/releases/latest).
|
||||
If you want to get a nightly build, go to the [Azure DevOps page](https://dev.azure.com/typefox/Arduino/_build?definitionId=4),
|
||||
and follow the steps from below.
|
||||
### FAQ
|
||||
|
||||
<img width="500" src="static/download_01.gif">
|
||||
<img width="500" src="static/download_02.gif">
|
||||
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
|
||||
- A: Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
|
||||
|
||||
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.
|
||||
- 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`.
|
||||
|
||||
### Architecture overview
|
||||
|
||||
The Pro IDE consists of three major parts:
|
||||
- the _Electron main_ process,
|
||||
- the _backend_, and
|
||||
- the _frontend_.
|
||||
|
||||
The _Electron main_ process is responsible for:
|
||||
- creating the application,
|
||||
- managing the application lifecycle via listeners, and
|
||||
- creating and managing the web pages for the app.
|
||||
|
||||
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process.
|
||||
|
||||
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
||||
|
||||
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the Pro IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the Pro IDE, only the _backend_ allows OS interaction.
|
||||
|
||||
The _backend_ process is responsible for:
|
||||
- providing access to the filesystem,
|
||||
- communicating with the Arduino CLI via gRPC,
|
||||
- running your terminal,
|
||||
- exposing additional RESTful APIs,
|
||||
- performing the Git commands in the local repositories,
|
||||
- hosting and running any VS Code extensions, or
|
||||
- executing VS Code tasks<sup>[[2]]</sup>.
|
||||
|
||||
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
|
||||
|
||||
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
|
||||
|
||||
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
|
||||
[2]: https://code.visualstudio.com/Docs/editor/tasks
|
||||
|
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"name": "arduino-debugger-extension",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.3",
|
||||
"description": "An extension for debugging Arduino programs",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/debug": "next",
|
||||
"arduino-ide-extension": "0.0.5",
|
||||
"arduino-ide-extension": "0.1.3",
|
||||
"cdt-gdb-adapter": "^0.0.14",
|
||||
"vscode-debugadapter": "^1.26.0",
|
||||
"vscode-debugprotocol": "^1.26.0"
|
||||
@@ -20,21 +17,14 @@
|
||||
"build": "tsc && yarn lint",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^2.6.1",
|
||||
"tslint": "^5.5.0",
|
||||
"typescript": "3.5.1"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"build",
|
||||
"data"
|
||||
"src"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/backend-module",
|
||||
"frontend": "lib/browser/frontend-module"
|
||||
"backend": "lib/node/arduino-debug-backend-module",
|
||||
"frontend": "lib/browser/arduino-debug-frontend-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MenuModelRegistry, Path, MessageService, Command, CommandRegistry } from '@theia/core';
|
||||
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';
|
||||
@@ -62,8 +62,8 @@ export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendAp
|
||||
if (current.configuration.type === 'arduino') {
|
||||
const wsStat = this.workspaceService.workspace;
|
||||
let sketchFileURI: URI | undefined;
|
||||
if (wsStat && await this.sketchesService.isSketchFolder(wsStat.uri)) {
|
||||
const wsPath = new Path(wsStat.uri);
|
||||
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) {
|
||||
@@ -114,7 +114,7 @@ export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendAp
|
||||
id: ArduinoDebugCommands.START_DEBUG.id,
|
||||
command: ArduinoDebugCommands.START_DEBUG.id,
|
||||
tooltip: 'Start Debugging',
|
||||
priority: 1
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,6 @@ 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);
|
@@ -2,13 +2,13 @@
|
||||
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 { BoardsServiceClientImpl } from 'arduino-ide-extension/lib/browser/boards/boards-service-client-impl';
|
||||
import { BoardsServiceProvider } from 'arduino-ide-extension/lib/browser/boards/boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoVariableResolver implements VariableContribution {
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService
|
||||
@@ -27,7 +27,7 @@ export class ArduinoVariableResolver implements VariableContribution {
|
||||
}
|
||||
|
||||
protected async resolveFqbn(): Promise<string | undefined> {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
this.messageService.error('No board selected. Please select a board for debugging.');
|
||||
return undefined;
|
||||
@@ -36,7 +36,7 @@ export class ArduinoVariableResolver implements VariableContribution {
|
||||
}
|
||||
|
||||
protected async resolvePort(): Promise<string | undefined> {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (!boardsConfig || !boardsConfig.selectedPort) {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ 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 { ArduinoCli } from 'arduino-ide-extension/lib/node/arduino-cli';
|
||||
import { ArduinoDaemonImpl } from 'arduino-ide-extension/lib/node/arduino-daemon-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
|
||||
@@ -12,7 +12,7 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
|
||||
readonly label = 'Arduino';
|
||||
readonly languages = ['c', 'cpp', 'ino'];
|
||||
|
||||
@inject(ArduinoCli) arduinoCli: ArduinoCli;
|
||||
@inject(ArduinoDaemonImpl) daemon: ArduinoDaemonImpl;
|
||||
|
||||
getSchemaAttributes(): IJSONSchema[] {
|
||||
return [
|
||||
@@ -66,7 +66,7 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
|
||||
const startFunction = config.pauseAtMain ? 'main' : 'setup';
|
||||
const res: ActualDebugConfig = {
|
||||
...config,
|
||||
arduinoCli: await this.arduinoCli.getExecPath(),
|
||||
arduinoCli: await this.daemon.getExecPath(),
|
||||
fqbn: '${fqbn}',
|
||||
uploadPort: '${port}',
|
||||
initCommands: [
|
||||
|
@@ -27,8 +27,10 @@ export class ArduinoGDBBackend extends GDBBackend {
|
||||
'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;
|
||||
|
@@ -50,3 +50,7 @@ The Config Service knows about your system, like for example the default sketch
|
||||
- [src/node/config-service-impl.ts](./src/node/config-service-impl.ts) implements the service backend:
|
||||
- getting the `arduino-cli` version and configuration
|
||||
- checking whether a file is in a data or sketch directory
|
||||
|
||||
### `"arduino"` configuration in the `package.json`:
|
||||
- `"cli"`:
|
||||
- `"version"` type `string` | `{ owner: string, repo: string, commitish?: string }`: if the type is a `string` and is a valid semver, it will get the corresponding [released](https://github.com/arduino/arduino-cli/releases) CLI. If the type is `string` and is a [date in `YYYYMMDD`](https://arduino.github.io/arduino-cli/latest/installation/#nightly-builds) format, it will get a nightly CLI. If the type is an object, a CLI, build from the sources in the `owner/repo` will be used. If `commitish` is not defined, the HEAD of the default branch will be used. In any other cases an error is thrown.
|
||||
|
137
arduino-ide-extension/data/cli/schema/arduino-cli.schema.json
Normal file
137
arduino-ide-extension/data/cli/schema/arduino-cli.schema.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"$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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,68 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.3",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^0.6.12",
|
||||
"@theia/application-package": "next",
|
||||
"@theia/core": "next",
|
||||
"@theia/cpp": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/git": "next",
|
||||
"@theia/languages": "next",
|
||||
"@theia/markers": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/google-protobuf": "^3.7.1",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@types/which": "^1.3.1",
|
||||
"css-element-queries": "^1.2.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"google-protobuf": "^3.11.0",
|
||||
"p-queue": "^5.0.0",
|
||||
"ps-tree": "^1.2.0",
|
||||
"react-select": "^3.0.4",
|
||||
"semver": "^6.3.0",
|
||||
"string-natural-compare": "^2.0.3",
|
||||
"tree-kill": "^1.2.1",
|
||||
"upath": "^1.1.2",
|
||||
"which": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
|
||||
"prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build",
|
||||
"clean": "rimraf lib",
|
||||
"download-cli": "node ./scripts/download-cli.js",
|
||||
"download-ls": "node ./scripts/download-ls.js",
|
||||
"download-examples": "node ./scripts/download-examples.js",
|
||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./test/**/*.test.ts\""
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
"reflect-metadata/Reflect"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
"watch-extensions": "ts,tsx",
|
||||
"timeout": 10000
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.1.1",
|
||||
"@theia/application-package": "next",
|
||||
"@theia/core": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/git": "next",
|
||||
"@theia/markers": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/preferences": "next",
|
||||
"@theia/output": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/ncp": "^2.0.4",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@types/sinon": "^7.5.2",
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"css-element-queries": "^1.2.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fuzzy": "^0.1.3",
|
||||
"glob": "^7.1.6",
|
||||
"google-protobuf": "^3.11.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"js-yaml": "^3.13.1",
|
||||
"ncp": "^2.0.0",
|
||||
"p-queue": "^5.0.0",
|
||||
"ps-tree": "^1.2.0",
|
||||
"react-select": "^3.0.4",
|
||||
"semver": "^7.3.2",
|
||||
"string-natural-compare": "^2.0.3",
|
||||
"temp": "^0.9.1",
|
||||
"tree-kill": "^1.2.1",
|
||||
"upath": "^1.1.2",
|
||||
"which": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.7",
|
||||
@@ -70,33 +74,53 @@
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"download": "^7.1.0",
|
||||
"grpc-tools": "^1.8.0",
|
||||
"grpc_tools_node_protoc_ts": "^2.5.8",
|
||||
"grpc_tools_node_protoc_ts": "^4.1.0",
|
||||
"mocha": "^7.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"protoc": "^1.0.4",
|
||||
"shelljs": "^0.8.3",
|
||||
"ts-node": "^8.6.2",
|
||||
"tslint": "^5.5.0",
|
||||
"typescript": "3.5.3",
|
||||
"sinon": "^9.0.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"grpc-tools": "^1.9.0"
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"reflect-metadata/Reflect"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
"watch-extensions": "js",
|
||||
"timeout": 10000
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"build",
|
||||
"data"
|
||||
"data",
|
||||
"examples"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/arduino-backend-module",
|
||||
"frontend": "lib/browser/arduino-frontend-module"
|
||||
"backend": "lib/node/arduino-ide-backend-module",
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/menu/browser-arduino-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
|
||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/boards/quick-open/boards-quick-open-module"
|
||||
},
|
||||
{
|
||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||
}
|
||||
]
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.14.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,61 +1,141 @@
|
||||
// @ts-check
|
||||
// The links to the downloads as of today (02.09.) are the followings:
|
||||
// In order to get the latest nightly build for your platform use the following links replacing <DATE> with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 )
|
||||
// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_64bit.tar.gz
|
||||
// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_ARM64.tar.gz
|
||||
// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Windows_64bit.zip
|
||||
// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_macOS_64bit.tar.gz
|
||||
// [...]
|
||||
// redirecting to latest generated builds by replacing latest with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 latest is replaced with 20190806
|
||||
|
||||
(() => {
|
||||
|
||||
const DEFAULT_VERSION = '0.9.0'; // require('moment')().format('YYYYMMDD');
|
||||
(async () => {
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = require('temp');
|
||||
const shell = require('shelljs');
|
||||
const semver = require('semver');
|
||||
const moment = require('moment');
|
||||
const downloader = require('./downloader');
|
||||
|
||||
const yargs = require('yargs')
|
||||
.option('cli-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_VERSION,
|
||||
describe: `The version of the 'arduino-cli' to download, or 'nightly-latest'. Defaults to ${DEFAULT_VERSION}.`
|
||||
})
|
||||
.option('force-download', {
|
||||
alias: 'fd',
|
||||
default: false,
|
||||
describe: `If set, this script force downloads the 'arduino-cli' even if it already exists on the file system.`
|
||||
})
|
||||
.version(false).parse();
|
||||
|
||||
const version = yargs['cli-version'];
|
||||
const force = yargs['force-download'];
|
||||
const { platform, arch } = process;
|
||||
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
|
||||
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
case 'darwin': return 'macOS_64bit.tar.gz';
|
||||
case 'win32': return 'Windows_64bit.zip';
|
||||
case 'linux': {
|
||||
switch (arch) {
|
||||
case 'arm64': return 'Linux_ARM64.tar.gz';
|
||||
case 'x64': return 'Linux_64bit.tar.gz';
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
default: return undefined;
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
if (!cli) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
return version;
|
||||
})();
|
||||
if (!suffix) {
|
||||
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
|
||||
|
||||
if (!version) {
|
||||
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const url = `https://downloads.arduino.cc/arduino-cli${version.startsWith('nightly-') ? '/nightly' : ''}/arduino-cli_${version}_${suffix}`;
|
||||
downloader.downloadUnzipFile(url, cli, 'arduino-cli', force);
|
||||
const { platform, arch } = process;
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
|
||||
const destinationPath = path.join(buildFolder, cliName);
|
||||
|
||||
if (typeof version === 'string') {
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
case 'darwin': return 'macOS_64bit.tar.gz';
|
||||
case 'win32': return 'Windows_64bit.zip';
|
||||
case 'linux': {
|
||||
switch (arch) {
|
||||
case 'arm': return 'Linux_ARMv7.tar.gz';
|
||||
case 'arm64': return 'Linux_ARM64.tar.gz';
|
||||
case 'x64': return 'Linux_64bit.tar.gz';
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
default: return undefined;
|
||||
}
|
||||
})();
|
||||
if (!suffix) {
|
||||
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (semver.valid(version)) {
|
||||
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
|
||||
shell.echo(`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
|
||||
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
|
||||
shell.echo(`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} else {
|
||||
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
} else {
|
||||
|
||||
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||
const { owner, repo, commitish } = version;
|
||||
if (!owner) {
|
||||
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (!repo) {
|
||||
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
const url = `https://github.com/${owner}/${repo}.git`;
|
||||
shell.echo(`Building CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
shell.echo(`Skipping the CLI build because it already exists: ${destinationPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
||||
shell.echo('Could not create build folder.');
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const tempRepoPath = temp.mkdirSync();
|
||||
shell.echo(`>>> Cloning CLI source to ${tempRepoPath}...`);
|
||||
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Cloned CLI repo.')
|
||||
|
||||
if (commitish) {
|
||||
shell.echo(`>>> Checking out ${commitish}...`);
|
||||
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out ${commitish}.`);
|
||||
}
|
||||
|
||||
shell.echo(`>>> Building the CLI...`);
|
||||
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< CLI build done.')
|
||||
|
||||
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
|
||||
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const builtCliPath = path.join(tempRepoPath, cliName);
|
||||
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
|
||||
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Copied the CLI.`);
|
||||
|
||||
shell.echo('<<< Verifying CLI...');
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('>>> Verified CLI.');
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
33
arduino-ide-extension/scripts/download-examples.js
Normal file
33
arduino-ide-extension/scripts/download-examples.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.0';
|
||||
|
||||
(async () => {
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const { v4 } = require('uuid');
|
||||
|
||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
|
||||
if (shell.mkdir('-p', repository).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const destination = path.join(__dirname, '..', 'Examples');
|
||||
shell.mkdir('-p', destination);
|
||||
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
|
||||
|
||||
})();
|
@@ -21,9 +21,9 @@ process.on('uncaughtException', error => {
|
||||
* @param url {string} Download URL
|
||||
* @param targetFile {string} Path to the file to copy from the decompressed archive
|
||||
* @param filePrefix {string} Prefix of the file name found in the archive
|
||||
* @param force {boolean} Whether to download even if the target file exists
|
||||
* @param force {boolean} Whether to download even if the target file exists. `false` by default.
|
||||
*/
|
||||
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
|
||||
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
|
@@ -2,80 +2,131 @@
|
||||
|
||||
(async () => {
|
||||
|
||||
const DEFAULT_VERSION = 'nightly'; // '0.3.7-alpha.preview';
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const { v4 } = require('uuid');
|
||||
const shell = require('shelljs');
|
||||
const protoc = path.dirname(require('protoc/protoc'));
|
||||
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${protoc}`;
|
||||
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
|
||||
const yargs = require('yargs')
|
||||
.option('cli-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_VERSION,
|
||||
choices: [
|
||||
// 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`.
|
||||
'nightly'
|
||||
],
|
||||
describe: `The version of the 'arduino-cli' to download. Specify either an existing version or use 'nightly'. Defaults to ${DEFAULT_VERSION}.`
|
||||
})
|
||||
.version(false).parse();
|
||||
|
||||
const version = yargs['cli-version'];
|
||||
if (version !== 'nightly') {
|
||||
shell.echo(`Only 'nightly' version is supported.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
|
||||
if (shell.mkdir('-p', repository).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
if (shell.exec(`git clone https://github.com/arduino/arduino-cli.git ${repository}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
if (version !== 'nightly') {
|
||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
||||
const { owner, repo, commitish } = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) {
|
||||
shell.echo(`Could not parse the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
if (!cli) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
if (!version) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
}
|
||||
|
||||
if (typeof version === 'string') {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
}
|
||||
|
||||
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||
const { owner, repo, commitish } = version;
|
||||
if (!owner) {
|
||||
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (!repo) {
|
||||
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
return { owner, repo, commitish };
|
||||
})();
|
||||
|
||||
const url = `https://github.com/${owner}/${repo}.git`;
|
||||
shell.echo(`>>> Cloning repository from '${url}'...`);
|
||||
if (shell.exec(`git clone ${url} ${repository}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('Generating TS/JS API from:');
|
||||
shell.echo(`<<< Repository cloned.`);
|
||||
|
||||
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) {
|
||||
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) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out version: ${commitish}.`);
|
||||
} else if (commitish) {
|
||||
shell.echo(`>>> Checking out commitish: ${commitish}...`);
|
||||
if (shell.exec(`git -C ${repository} checkout ${commitish}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out commitish: ${commitish}.`);
|
||||
}
|
||||
|
||||
shell.echo('>>> Generating TS/JS API from:');
|
||||
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
if (shell.exec(`git -C ${repository} rev-parse --short HEAD`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const pluginExec = shell.which('grpc_tools_node_protoc_plugin');
|
||||
if (!pluginExec || pluginExec.code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
const plugin = pluginExec.stdout.trim();
|
||||
|
||||
const rpc = path.join(repository, 'rpc');
|
||||
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
|
||||
shell.mkdir('-p', out);
|
||||
|
||||
const protos = await new Promise(resolve =>
|
||||
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
|
||||
if (error) {
|
||||
shell.echo(error.stack);
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
resolve(matches.map(filename => path.join(rpc, filename)));
|
||||
}));
|
||||
if (!protos || protos.length === 0) {
|
||||
shell.echo(`Could not find any .proto files under ${rpc}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
// Generate JS code from the `.proto` files.
|
||||
if (shell.exec(`grpc_tools_node_protoc \
|
||||
--js_out=import_style=commonjs,binary:${out} \
|
||||
--grpc_out=${out} \
|
||||
--plugin=protoc-gen-grpc=${plugin} \
|
||||
--grpc_out=generate_package_definition:${out} \
|
||||
-I ${rpc} \
|
||||
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
|
||||
${protos.join(' ')}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
// Generate the `.d.ts` files for JS.
|
||||
if (shell.exec(`protoc \
|
||||
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', 'protoc-gen-ts')} \
|
||||
--ts_out=${out} \
|
||||
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`)} \
|
||||
--ts_out=generate_package_definition:${out} \
|
||||
-I ${rpc} \
|
||||
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
|
||||
${protos.join(' ')}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const { patch } = require('./patch-grpc-js');
|
||||
patch([out])
|
||||
shell.echo('Done.');
|
||||
shell.echo('<<< Generation was successful.');
|
||||
|
||||
})();
|
||||
})();
|
||||
|
@@ -1,38 +0,0 @@
|
||||
// Use `@grpc/grpc-js` instead of `grpc` at runtime.
|
||||
// https://github.com/grpc/grpc-node/issues/624
|
||||
// https://github.com/grpc/grpc-node/issues/931
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports.patch = function (roots = [path.join(__dirname, '..', 'src', 'node')]) {
|
||||
console.info('🔧 <<< Patching code...');
|
||||
patch(roots);
|
||||
console.info('👌 <<< Done. The code has been patched.');
|
||||
};
|
||||
|
||||
function patch(paths) {
|
||||
for (const p of paths) {
|
||||
const exist = fs.existsSync(p);
|
||||
if (exist) {
|
||||
const stat = fs.statSync(p);
|
||||
if (stat.isDirectory()) {
|
||||
console.info(`🔧 >>> Scanning code in ${p}...`);
|
||||
patch(fs.readdirSync(p).map(name => path.join(p, name)));
|
||||
} else {
|
||||
let content = fs.readFileSync(p, { encoding: 'utf8' });
|
||||
if (content.indexOf("require('grpc')") !== -1) {
|
||||
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${p}.`);
|
||||
fs.writeFileSync(p, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
|
||||
}
|
||||
content = fs.readFileSync(p, { encoding: 'utf8' });
|
||||
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
|
||||
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${p}.`);
|
||||
fs.writeFileSync(p, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`${p} does not exist. Skipping.`);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,58 +1,30 @@
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
|
||||
/**
|
||||
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
|
||||
*/
|
||||
export namespace ArduinoCommands {
|
||||
|
||||
export const VERIFY: Command = {
|
||||
id: 'arduino-verify',
|
||||
label: 'Verify Sketch'
|
||||
}
|
||||
export const VERIFY_TOOLBAR: Command = {
|
||||
id: 'arduino-verify-toolbar',
|
||||
}
|
||||
|
||||
export const UPLOAD: Command = {
|
||||
id: 'arduino-upload',
|
||||
label: 'Upload Sketch'
|
||||
}
|
||||
export const UPLOAD_TOOLBAR: Command = {
|
||||
id: 'arduino-upload-toolbar',
|
||||
}
|
||||
|
||||
export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
|
||||
id: "arduino-toggle-compile-for-debug"
|
||||
}
|
||||
id: 'arduino-toggle-compile-for-debug'
|
||||
};
|
||||
|
||||
export const SHOW_OPEN_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-show-open-context-menu',
|
||||
label: 'Open Sketch'
|
||||
}
|
||||
|
||||
export const OPEN_FILE_NAVIGATOR: Command = {
|
||||
id: 'arduino-open-file-navigator'
|
||||
}
|
||||
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-file'
|
||||
}
|
||||
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-file'
|
||||
}
|
||||
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: "arduino-new-sketch",
|
||||
label: 'New Sketch',
|
||||
category: 'File'
|
||||
}
|
||||
/**
|
||||
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
|
||||
*/
|
||||
export const OPEN_SKETCH_FILES: Command = {
|
||||
id: 'arduino-open-sketch-files'
|
||||
};
|
||||
|
||||
export const OPEN_BOARDS_DIALOG: Command = {
|
||||
id: "arduino-open-boards-dialog"
|
||||
}
|
||||
id: 'arduino-open-boards-dialog'
|
||||
};
|
||||
|
||||
export const TOGGLE_ADVANCED_MODE: Command = {
|
||||
id: "arduino-toggle-advanced-mode"
|
||||
}
|
||||
id: 'arduino-toggle-advanced-mode'
|
||||
};
|
||||
export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = {
|
||||
id: "arduino-toggle-advanced-mode-toolbar"
|
||||
}
|
||||
id: 'arduino-toggle-advanced-mode-toolbar'
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -1,65 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { WorkspaceServiceExt } from './workspace-service-ext';
|
||||
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
|
||||
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } from '@theia/core';
|
||||
import {
|
||||
ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
|
||||
FrontendApplication, KeybindingContribution, KeybindingRegistry
|
||||
ContextMenuRenderer,
|
||||
FrontendApplication, FrontendApplicationContribution,
|
||||
OpenerService, StatusBar, StatusBarAlignment
|
||||
} from '@theia/core/lib/browser';
|
||||
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
|
||||
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
|
||||
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { ArduinoWorkspaceService } from './arduino-workspace-service';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorMainMenu, EditorManager } from '@theia/editor/lib/browser';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import * as React from 'react';
|
||||
import { MainMenuManager } from '../common/main-menu-manager';
|
||||
import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol';
|
||||
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { FileSystemExt } from '../common/protocol/filesystem-ext';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ArduinoMenus } from './menu/arduino-menus';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { 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';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
|
||||
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
|
||||
}
|
||||
|
||||
export namespace ArduinoToolbarContextMenu {
|
||||
export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu'];
|
||||
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
|
||||
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
|
||||
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
|
||||
}
|
||||
const debounce = require('lodash.debounce');
|
||||
import { OutputService } from '../common/protocol/output-service';
|
||||
import { NotificationCenter } from './notification-center';
|
||||
import { Settings } from './contributions/settings';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
|
||||
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution, ColorContribution {
|
||||
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@@ -70,14 +64,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(WorkspaceServiceExt)
|
||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||
|
||||
@inject(ToolOutputServiceClient)
|
||||
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
@@ -91,8 +79,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
@inject(FileService)
|
||||
protected readonly fileSystem: FileService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
@@ -109,8 +97,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(ArduinoWorkspaceService)
|
||||
protected readonly workspaceService: ArduinoWorkspaceService;
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
@@ -136,33 +124,62 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected application: FrontendApplication;
|
||||
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected executableService: ExecutableService;
|
||||
@inject(OutputService)
|
||||
protected readonly outputService: OutputService;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
// This is a hack. Otherwise, the backend services won't bind.
|
||||
await this.workspaceServiceExt.roots();
|
||||
|
||||
const updateStatusBar = (config: BoardsConfig.Config) => {
|
||||
if (!window.navigator.onLine) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
|
||||
}
|
||||
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
|
||||
this.statusBar.setElement('arduino-selected-board', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: BoardsConfig.Config.toString(config)
|
||||
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
|
||||
className: 'arduino-selected-board'
|
||||
});
|
||||
if (selectedBoard) {
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
|
||||
className: 'arduino-selected-port'
|
||||
});
|
||||
}
|
||||
}
|
||||
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClient.boardsConfig);
|
||||
|
||||
this.registerSketchesInMenu(this.menuRegistry);
|
||||
|
||||
Promise.all([
|
||||
this.boardsService.getAttachedBoards(),
|
||||
this.boardsService.getAvailablePorts()
|
||||
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.application = app;
|
||||
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
|
||||
for (const viewContribution of [
|
||||
this.fileNavigatorContributions,
|
||||
@@ -171,184 +188,97 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
this.problemContribution,
|
||||
this.scmContribution,
|
||||
this.siwContribution] as Array<FrontendApplicationContribution>) {
|
||||
|
||||
if (viewContribution.initializeLayout) {
|
||||
viewContribution.initializeLayout(this.application);
|
||||
viewContribution.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(async ({ selectedBoard }) => {
|
||||
if (selectedBoard) {
|
||||
const { name, fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
await this.hostedPluginSupport.didStart;
|
||||
this.startLanguageServer(fqbn, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 { clangdUri, cliUri, lsUri } = await this.executableService.list();
|
||||
const [clangdPath, cliPath, lsPath] = await Promise.all([
|
||||
this.fileSystem.fsPath(new URI(clangdUri)),
|
||||
this.fileSystem.fsPath(new URI(cliUri)),
|
||||
this.fileSystem.fsPath(new URI(lsUri)),
|
||||
]);
|
||||
this.commandRegistry.executeCommand('arduino.languageserver.start', {
|
||||
lsPath,
|
||||
cliPath,
|
||||
clangdPath,
|
||||
board: {
|
||||
fqbn,
|
||||
name: name ? `"${name}"` : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.VERIFY.id,
|
||||
command: ArduinoCommands.VERIFY_TOOLBAR.id,
|
||||
tooltip: 'Verify'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.UPLOAD.id,
|
||||
command: ArduinoCommands.UPLOAD_TOOLBAR.id,
|
||||
tooltip: 'Upload'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
|
||||
command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
|
||||
tooltip: 'Open',
|
||||
priority: 2
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.SAVE_SKETCH.id,
|
||||
command: ArduinoCommands.SAVE_SKETCH.id,
|
||||
tooltip: 'Save',
|
||||
priority: 2
|
||||
});
|
||||
registry.registerItem({
|
||||
id: BoardsToolBarItem.TOOLBAR_ID,
|
||||
render: () => <BoardsToolBarItem
|
||||
key='boardsToolbarItem'
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClient}
|
||||
boardService={this.boardsService} />,
|
||||
boardsServiceClient={this.boardsServiceClientImpl} />,
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
priority: 2
|
||||
priority: 7
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'toggle-serial-monitor',
|
||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||
tooltip: 'Toggle Serial Monitor'
|
||||
tooltip: 'Serial Monitor'
|
||||
});
|
||||
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
|
||||
command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id,
|
||||
tooltip: 'Toggle Advanced Mode',
|
||||
text: (this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)')
|
||||
tooltip: this.editorMode.proMode ? 'Switch to Classic Mode' : 'Switch to Advanced Mode',
|
||||
text: this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)'
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
// TODO: use proper API https://github.com/eclipse-theia/theia/pull/6599
|
||||
const allHandlers: { [id: string]: CommandHandler[] } = (registry as any)._handlers;
|
||||
|
||||
// Make sure to reveal the `Explorer` before executing `New File` and `New Folder`.
|
||||
for (const command of [WorkspaceCommands.NEW_FILE, WorkspaceCommands.NEW_FOLDER]) {
|
||||
const { id } = command;
|
||||
const handlers = allHandlers[id].slice();
|
||||
registry.unregisterCommand(id);
|
||||
registry.registerCommand(command);
|
||||
for (const handler of handlers) {
|
||||
const wrapper: CommandHandler = {
|
||||
execute: (...args: any[]) => {
|
||||
this.fileNavigatorContributions.openView({ reveal: true }).then(() => handler.execute(args));
|
||||
},
|
||||
isVisible: (...args: any[]) => {
|
||||
return handler.isVisible!(args);
|
||||
},
|
||||
isEnabled: (args: any[]) => {
|
||||
return handler.isEnabled!(args);
|
||||
},
|
||||
isToggled: (args: any[]) => {
|
||||
return handler.isToggled!(args);
|
||||
}
|
||||
};
|
||||
if (!handler.isEnabled) {
|
||||
delete wrapper.isEnabled;
|
||||
}
|
||||
if (!handler.isToggled) {
|
||||
delete wrapper.isToggled;
|
||||
}
|
||||
if (!handler.isVisible) {
|
||||
delete wrapper.isVisible;
|
||||
}
|
||||
registry.registerHandler(id, wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
registry.registerCommand(ArduinoCommands.VERIFY, {
|
||||
execute: this.verify.bind(this)
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.VERIFY_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: this.verify.bind(this)
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
||||
execute: () => {
|
||||
this.editorMode.toggleCompileForDebug();
|
||||
this.editorMode.menuContentChanged.fire();
|
||||
},
|
||||
execute: () => this.editorMode.toggleCompileForDebug(),
|
||||
isToggled: () => this.editorMode.compileForDebug
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.UPLOAD, {
|
||||
execute: this.upload.bind(this)
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.UPLOAD_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: this.upload.bind(this)
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (widget: Widget, target: EventTarget) => {
|
||||
if (this.wsSketchCount) {
|
||||
const el = (target as HTMLElement).parentElement;
|
||||
if (el) {
|
||||
this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, {
|
||||
x: el.getBoundingClientRect().left,
|
||||
y: el.getBoundingClientRect().top + el.offsetHeight
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
|
||||
}
|
||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
|
||||
execute: async (uri: URI) => {
|
||||
this.openSketchFiles(uri);
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, {
|
||||
isEnabled: () => true,
|
||||
execute: () => this.doOpenFile()
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
|
||||
isEnabled: () => true,
|
||||
execute: async (sketch: Sketch) => {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (sketch: Sketch) => {
|
||||
registry.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
|
||||
execute: async uri => {
|
||||
try {
|
||||
// hack: sometimes we don't get the workspace root, but the currently active file: correct for that
|
||||
if (uri.path.ext !== "") {
|
||||
uri = uri.withPath(uri.path.dir.dir);
|
||||
}
|
||||
|
||||
const sketch = await this.sketchService.createNewSketch(uri.toString());
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
|
||||
isEnabled: () => true,
|
||||
execute: async () => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open();
|
||||
execute: async (query?: string | undefined) => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open(query);
|
||||
if (boardsConfig) {
|
||||
this.boardsServiceClient.boardsConfig = boardsConfig;
|
||||
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
|
||||
isToggled: () => this.editorMode.proMode,
|
||||
execute: () => this.editorMode.toggleProMode()
|
||||
@@ -360,256 +290,51 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
});
|
||||
}
|
||||
|
||||
protected async verify() {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
if (!boardsConfig.selectedBoard.fqbn) {
|
||||
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
await this.coreService.compile({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected async upload() {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
const { selectedPort } = boardsConfig;
|
||||
if (!selectedPort) {
|
||||
throw new Error('No ports selected. Please select a port.');
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
await this.coreService.upload({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
port: selectedPort.address,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry) {
|
||||
if (!this.editorMode.proMode) {
|
||||
// If are not in pro-mode, we have to disable the context menu for the tabs.
|
||||
// Such as `Close`, `Close All`, etc.
|
||||
for (const command of [
|
||||
CommonCommands.CLOSE_TAB,
|
||||
CommonCommands.CLOSE_OTHER_TABS,
|
||||
CommonCommands.CLOSE_RIGHT_TABS,
|
||||
CommonCommands.CLOSE_ALL_TABS,
|
||||
CommonCommands.COLLAPSE_PANEL,
|
||||
CommonCommands.TOGGLE_MAXIMIZED,
|
||||
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
|
||||
]) {
|
||||
registry.unregisterMenuAction(command);
|
||||
const menuId = (menuPath: string[]): string => {
|
||||
const index = menuPath.length - 1;
|
||||
const menuId = menuPath[index];
|
||||
return menuId;
|
||||
}
|
||||
|
||||
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
|
||||
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
|
||||
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.CLOSE);
|
||||
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION));
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO));
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL));
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW));
|
||||
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, {
|
||||
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
|
||||
label: 'Optimize for Debugging',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
commandId: ArduinoCommands.VERIFY.id,
|
||||
label: 'Verify/Compile',
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
commandId: ArduinoCommands.UPLOAD.id,
|
||||
label: 'Upload',
|
||||
order: '3'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, {
|
||||
commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id,
|
||||
label: 'Open...'
|
||||
});
|
||||
|
||||
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
|
||||
|
||||
registry.registerMenuAction(CommonMenus.HELP, {
|
||||
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
|
||||
label: 'Advanced Mode'
|
||||
});
|
||||
|
||||
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sketch'], {
|
||||
commandId: ArduinoCommands.NEW_SKETCH.id
|
||||
});
|
||||
}
|
||||
|
||||
protected getMenuId(menuPath: string[]): string {
|
||||
const index = menuPath.length - 1;
|
||||
const menuId = menuPath[index];
|
||||
return menuId;
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: ArduinoCommands.VERIFY.id,
|
||||
keybinding: 'ctrlcmd+alt+v'
|
||||
});
|
||||
keybindings.registerKeybinding({
|
||||
command: ArduinoCommands.UPLOAD.id,
|
||||
keybinding: 'ctrlcmd+alt+u'
|
||||
});
|
||||
}
|
||||
|
||||
protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
|
||||
this.sketchService.getSketches().then(sketches => {
|
||||
this.wsSketchCount = sketches.length;
|
||||
sketches.forEach(sketch => {
|
||||
const command: Command = {
|
||||
id: 'openSketch' + sketch.name
|
||||
}
|
||||
this.commandRegistry.registerCommand(command, {
|
||||
execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
|
||||
commandId: command.id,
|
||||
label: sketch.name
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async openSketchFiles(uri: string): Promise<void> {
|
||||
this.sketchService.getSketchFiles(uri).then(uris => {
|
||||
for (const uri of uris) {
|
||||
this.editorManager.open(new URI(uri));
|
||||
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]) {
|
||||
await this.ensureOpened(uri);
|
||||
}
|
||||
});
|
||||
await this.ensureOpened(mainFileUri, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
||||
this.messageService.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file after prompting the `Open File` dialog. Resolves to `undefined`, if
|
||||
* - the workspace root is not set,
|
||||
* - the file to open does not exist, or
|
||||
* - it was not a file, but a directory.
|
||||
*
|
||||
* Otherwise, resolves to the URI of the file.
|
||||
*/
|
||||
protected async doOpenFile(): Promise<URI | undefined> {
|
||||
const props: OpenFileDialogProps = {
|
||||
title: WorkspaceCommands.OPEN_FILE.dialogLabel,
|
||||
canSelectFolders: false,
|
||||
canSelectFiles: true
|
||||
};
|
||||
const [rootStat] = await this.workspaceService.roots;
|
||||
const destinationFileUri = await this.fileDialogService.showOpenDialog(props, rootStat);
|
||||
if (destinationFileUri) {
|
||||
const destinationFile = await this.fileSystem.getFileStat(destinationFileUri.toString());
|
||||
if (destinationFile && !destinationFile.isDirectory) {
|
||||
const message = await this.validate(destinationFile);
|
||||
if (!message) {
|
||||
this.workspaceService.open(destinationFileUri);
|
||||
return destinationFileUri;
|
||||
} else {
|
||||
this.messageService.warn(message);
|
||||
}
|
||||
}
|
||||
protected async ensureOpened(uri: string, forceOpen: boolean = false): Promise<any> {
|
||||
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(new URI(uri));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getCurrentWidget(): EditorWidget | undefined {
|
||||
let widget = this.editorManager.currentEditor;
|
||||
if (!widget) {
|
||||
const visibleWidgets = this.editorManager.all.filter(w => w.isVisible);
|
||||
if (visibleWidgets.length > 0) {
|
||||
widget = visibleWidgets[0];
|
||||
}
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `undefined` if the `file` is valid. Otherwise, returns with the validation error message.
|
||||
*/
|
||||
protected validate(file: FileStat): MaybePromise<string | undefined> {
|
||||
const uri = new URI(file.uri);
|
||||
const path = uri.path;
|
||||
const { name, ext, dir } = path;
|
||||
if (ext !== '.ino') {
|
||||
return "Only sketches with '.ino' extension can be opened.";
|
||||
}
|
||||
if (name !== dir.name) {
|
||||
return `The file "${name}${ext}" needs to be inside a sketch folder named "${name}".`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private toUri(arg: any): URI | undefined {
|
||||
if (arg instanceof URI) {
|
||||
return arg;
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
return new URI(arg);
|
||||
}
|
||||
if (arg instanceof EditorWidget) {
|
||||
return arg.editor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
@@ -638,6 +363,42 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
hc: 'editorWidget.background'
|
||||
},
|
||||
description: 'Color of the Arduino Pro IDE foreground which is used for dialogs, such as the Select Board dialog.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.background',
|
||||
defaults: {
|
||||
dark: 'button.background',
|
||||
light: 'button.background',
|
||||
hc: 'activityBar.inactiveForeground'
|
||||
},
|
||||
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'button.hoverBackground',
|
||||
light: 'button.hoverBackground',
|
||||
hc: 'activityBar.inactiveForeground'
|
||||
},
|
||||
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.foreground',
|
||||
defaults: {
|
||||
dark: 'editor.foreground',
|
||||
light: 'editor.foreground',
|
||||
hc: 'editor.foreground'
|
||||
},
|
||||
description: 'Color of the text in the Output view.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.background',
|
||||
defaults: {
|
||||
dark: 'editor.background',
|
||||
light: 'editor.background',
|
||||
hc: 'editor.background'
|
||||
},
|
||||
description: 'Background color of the Output view.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -1,244 +0,0 @@
|
||||
import '../../src/browser/style/index.css';
|
||||
import { ContainerModule, interfaces } from 'inversify';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'
|
||||
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
|
||||
import { LanguageClientContribution } from '@theia/languages/lib/browser';
|
||||
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
|
||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { WorkspaceServiceExt, WorkspaceServiceExtPath } from './workspace-service-ext';
|
||||
import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
|
||||
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||
import { ToolOutputService } from '../common/protocol/tool-output-service';
|
||||
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
|
||||
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ArduinoWorkspaceService } from './arduino-workspace-service';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { ArduinoProblemContribution } from './customization/arduino-problem-contribution';
|
||||
import { ArduinoNavigatorContribution } from './customization/arduino-navigator-contribution';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
|
||||
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
import { ArduinoOutputToolContribution } from './customization/arduino-output-tool-contribution';
|
||||
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
|
||||
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
|
||||
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
|
||||
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
|
||||
import { ApplicationShell, ShellLayoutRestorer, KeybindingContribution } from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { ArduinoApplicationShell } from './customization/arduino-application-shell';
|
||||
import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
|
||||
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
|
||||
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { ArduinoScmContribution } from './customization/arduino-scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { ArduinoSearchInWorkspaceContribution } from './customization/arduino-search-in-workspace-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorModel } from './monitor/monitor-model';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { ArduinoMonacoEditorProvider } from './editor/arduino-monaco-editor-provider';
|
||||
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
|
||||
import { ProblemManager } from '@theia/markers/lib/browser';
|
||||
import { ArduinoProblemManager } from './markers/arduino-problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ListItemRenderer } from './components/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduinoTheme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/arduino.color-theme.json')
|
||||
});
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
|
||||
ElementQueries.listen();
|
||||
ElementQueries.init();
|
||||
|
||||
// Commands and toolbar items
|
||||
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||
bind(MenuContribution).toService(ArduinoFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
|
||||
bind(KeybindingContribution).toService(ArduinoFrontendContribution);
|
||||
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
|
||||
bind(ColorContribution).toService(ArduinoFrontendContribution);
|
||||
|
||||
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
|
||||
|
||||
// `ino` TextMate grammar and language client
|
||||
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
|
||||
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
|
||||
// Library list widget
|
||||
bind(LibraryListWidget).toSelf();
|
||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(LibraryListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
||||
|
||||
// Config service
|
||||
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService).toDynamicValue(context => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(BoardsServiceClientImpl);
|
||||
return connection.createProxy(BoardsServicePath, client);
|
||||
}).inSingletonScope();
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
|
||||
bind(BoardsServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(BoardsServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
// boards auto-installer
|
||||
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
|
||||
|
||||
// Boards list widget
|
||||
bind(BoardsListWidget).toSelf();
|
||||
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(BoardsListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialogProps).toConstantValue({
|
||||
title: 'Select Board'
|
||||
})
|
||||
|
||||
// Core service
|
||||
bind(CoreService)
|
||||
.toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath))
|
||||
.inSingletonScope();
|
||||
|
||||
// Tool output service client
|
||||
bind(ToolOutputServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(ToolOutputServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(ToolOutputServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, ToolOutputService.SERVICE_PATH, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
// The workspace service extension
|
||||
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt: WorkspaceServiceExt) => {
|
||||
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
|
||||
// Eagerly active the core, library, and boards services.
|
||||
container.get(CoreService);
|
||||
container.get(LibraryService);
|
||||
container.get(BoardsService);
|
||||
container.get(SketchesService);
|
||||
return workspaceServiceExt;
|
||||
});
|
||||
|
||||
// Serial Monitor
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(MonitorWidget).toSelf();
|
||||
bindViewContribution(bind, MonitorViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: MonitorWidget.ID,
|
||||
createWidget: () => context.container.get(MonitorWidget)
|
||||
}));
|
||||
// Frontend binding for the monitor service
|
||||
bind(MonitorService).toDynamicValue(context => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
}).inSingletonScope();
|
||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
||||
// Monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
|
||||
|
||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
||||
bind(EditorMode).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
||||
rebind(OutlineViewContribution).to(ArduinoOutlineViewContribution).inSingletonScope();
|
||||
rebind(ProblemContribution).to(ArduinoProblemContribution).inSingletonScope();
|
||||
rebind(FileNavigatorContribution).to(ArduinoNavigatorContribution).inSingletonScope();
|
||||
rebind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
|
||||
rebind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
|
||||
rebind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
|
||||
rebind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
|
||||
rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope();
|
||||
rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope();
|
||||
rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
|
||||
|
||||
// Monaco customizations
|
||||
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
|
||||
rebind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
|
||||
|
||||
// Decorator customizations
|
||||
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
|
||||
rebind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
|
||||
|
||||
// Problem markers
|
||||
bind(ArduinoProblemManager).toSelf().inSingletonScope();
|
||||
rebind(ProblemManager).toService(ArduinoProblemManager);
|
||||
|
||||
// About dialog to show the CLI version
|
||||
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
|
||||
rebind(AboutDialog).toService(ArduinoAboutDialog);
|
||||
|
||||
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
|
||||
bind(ArduinoShellLayoutRestorer).toSelf().inSingletonScope();
|
||||
rebind(ShellLayoutRestorer).toService(ArduinoShellLayoutRestorer);
|
||||
});
|
364
arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Normal file
364
arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import '../../src/browser/style/index.css';
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { TabBarToolbarContribution, TabBarToolbarFactory } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import { FrontendApplicationContribution, FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
|
||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { OutlineViewContribution } from './theia/outline/outline-contribution';
|
||||
import { ProblemContribution as TheiaProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { ProblemContribution } from './theia/markers/problem-contribution';
|
||||
import { FileNavigatorContribution } from './theia/navigator/navigator-contribution';
|
||||
import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
|
||||
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
|
||||
import { EditorContribution } from './theia/editor/editor-contribution';
|
||||
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
|
||||
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
|
||||
import {
|
||||
ApplicationShell as TheiaApplicationShell,
|
||||
ShellLayoutRestorer as TheiaShellLayoutRestorer,
|
||||
CommonFrontendContribution as TheiaCommonFrontendContribution,
|
||||
KeybindingRegistry as TheiaKeybindingRegistry,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { ApplicationShell } from './theia/core/application-shell';
|
||||
import { FrontendApplication } from './theia/core/frontend-application';
|
||||
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
|
||||
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
||||
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { ScmContribution } from './theia/scm/scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorModel } from './monitor/monitor-model';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||
import { ProblemManager } from './theia/markers/problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
||||
import { EditorManager as TheiaEditorManager, EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
|
||||
import { EditorManager } from './theia/editor/editor-manager';
|
||||
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
|
||||
import {
|
||||
WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution,
|
||||
FileMenuContribution as TheiaFileMenuContribution,
|
||||
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution
|
||||
} from '@theia/workspace/lib/browser';
|
||||
import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './theia/workspace/workspace-frontend-contribution';
|
||||
import { Contribution } from './contributions/contribution';
|
||||
import { NewSketch } from './contributions/new-sketch';
|
||||
import { OpenSketch } from './contributions/open-sketch';
|
||||
import { CloseSketch } from './contributions/close-sketch';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
import { SaveSketch } from './contributions/save-sketch';
|
||||
import { VerifySketch } from './contributions/verify-sketch';
|
||||
import { UploadSketch } from './contributions/upload-sketch';
|
||||
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
|
||||
import { EditContributions } from './contributions/edit-contributions';
|
||||
import { OpenSketchExternal } from './contributions/open-sketch-external';
|
||||
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preferences-contribution';
|
||||
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
|
||||
import { QuitApp } from './contributions/quit-app';
|
||||
import { SketchControl } from './contributions/sketch-control';
|
||||
import { Settings } from './contributions/settings';
|
||||
import { KeybindingRegistry } from './theia/core/keybindings';
|
||||
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
||||
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
|
||||
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
|
||||
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
|
||||
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { OutputWidget } from './theia/output/output-widget';
|
||||
import { BurnBootloader } from './contributions/burn-bootloader';
|
||||
import { ExamplesServicePath, ExamplesService } from '../common/protocol/examples-service';
|
||||
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
|
||||
import { IncludeLibrary } from './contributions/include-library';
|
||||
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { OutputChannelManager } from './theia/output/output-channel';
|
||||
import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, OutputChannelRegistryMainImpl } from './theia/plugin-ext/output-channel-registry-main';
|
||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
|
||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
||||
import { OutputServiceImpl } from './output-service-impl';
|
||||
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
|
||||
import { NotificationCenter } from './notification-center';
|
||||
import { NotificationServicePath, NotificationServiceServer } from '../common/protocol';
|
||||
import { About } from './contributions/about';
|
||||
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
|
||||
import { TabBarRenderer } from './theia/core/tab-bars';
|
||||
import { EditorCommandContribution } from './theia/editor/editor-command';
|
||||
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
|
||||
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
|
||||
import { Debug } from './contributions/debug';
|
||||
import { DebugSessionManager } from './theia/debug/debug-session-manager';
|
||||
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduino-theme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/arduino.color-theme.json')
|
||||
});
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
ElementQueries.listen();
|
||||
ElementQueries.init();
|
||||
|
||||
// Commands and toolbar items
|
||||
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||
bind(MenuContribution).toService(ArduinoFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
|
||||
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
|
||||
bind(ColorContribution).toService(ArduinoFrontendContribution);
|
||||
|
||||
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
|
||||
|
||||
// Library list widget
|
||||
bind(LibraryListWidget).toSelf();
|
||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(LibraryListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
||||
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
|
||||
|
||||
// Config service
|
||||
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
||||
|
||||
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
||||
bind(FrontendApplicationContribution).to(BoardsDataMenuUpdater).inSingletonScope();
|
||||
bind(BoardsDataStore).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsDataStore);
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('store');
|
||||
}).inSingletonScope().whenTargetNamed('store');
|
||||
|
||||
// Boards auto-installer
|
||||
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
|
||||
|
||||
// Boards list widget
|
||||
bind(BoardsListWidget).toSelf();
|
||||
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(BoardsListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialogProps).toConstantValue({
|
||||
title: 'Select Board'
|
||||
})
|
||||
|
||||
// Core service
|
||||
bind(CoreService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)).inSingletonScope();
|
||||
|
||||
// Serial monitor
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(MonitorWidget).toSelf();
|
||||
bindViewContribution(bind, MonitorViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: MonitorWidget.ID,
|
||||
createWidget: () => context.container.get(MonitorWidget)
|
||||
}));
|
||||
// Frontend binding for the serial monitor service
|
||||
bind(MonitorService).toDynamicValue(context => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
}).inSingletonScope();
|
||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
||||
// Serial monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
||||
|
||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
||||
bind(EditorMode).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
||||
|
||||
// Layout and shell customizations.
|
||||
rebind(TheiaOutlineViewContribution).to(OutlineViewContribution).inSingletonScope();
|
||||
rebind(TheiaProblemContribution).to(ProblemContribution).inSingletonScope();
|
||||
rebind(TheiaFileNavigatorContribution).to(FileNavigatorContribution).inSingletonScope();
|
||||
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
|
||||
rebind(TheiaMonacoStatusBarContribution).to(MonacoStatusBarContribution).inSingletonScope();
|
||||
rebind(TheiaApplicationShell).to(ApplicationShell).inSingletonScope();
|
||||
rebind(TheiaScmContribution).to(ScmContribution).inSingletonScope();
|
||||
rebind(TheiaSearchInWorkspaceFrontendContribution).to(SearchInWorkspaceFrontendContribution).inSingletonScope();
|
||||
rebind(TheiaFrontendApplication).to(FrontendApplication).inSingletonScope();
|
||||
rebind(TheiaWorkspaceFrontendContribution).to(WorkspaceFrontendContribution).inSingletonScope();
|
||||
rebind(TheiaFileMenuContribution).to(ArduinoFileMenuContribution).inSingletonScope();
|
||||
rebind(TheiaCommonFrontendContribution).to(CommonFrontendContribution).inSingletonScope();
|
||||
rebind(TheiaPreferencesContribution).to(PreferencesContribution).inSingletonScope();
|
||||
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
|
||||
rebind(TheiaWorkspaceCommandContribution).to(WorkspaceCommandContribution).inSingletonScope();
|
||||
rebind(TheiaWorkspaceDeleteHandler).to(WorkspaceDeleteHandler).inSingletonScope();
|
||||
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
||||
rebind(TabBarToolbarFactory).toFactory(({ container: parentContainer }) => () => {
|
||||
const container = parentContainer.createChild();
|
||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||
return container.get(TabBarToolbar);
|
||||
});
|
||||
bind(OutputWidget).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||
rebind(TheiaOutputChannelRegistryMainImpl).toService(OutputChannelRegistryMainImpl);
|
||||
bind(MonacoTextModelService).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService);
|
||||
|
||||
// Show a disconnected status bar, when the daemon is not available
|
||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaApplicationConnectionStatusContribution).toService(ApplicationConnectionStatusContribution);
|
||||
bind(FrontendConnectionStatusService).toSelf().inSingletonScope();
|
||||
rebind(TheiaFrontendConnectionStatusService).toService(FrontendConnectionStatusService);
|
||||
|
||||
// Editor customizations. Sets the editor to `readOnly` if under the data dir.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
|
||||
// Decorator customizations
|
||||
bind(TabBarDecoratorService).toSelf().inSingletonScope();
|
||||
rebind(TheiaTabBarDecoratorService).toService(TabBarDecoratorService);
|
||||
|
||||
// Problem markers
|
||||
bind(ProblemManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaProblemManager).toService(ProblemManager);
|
||||
|
||||
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
|
||||
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
|
||||
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
|
||||
|
||||
bind(ArduinoDaemon).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath)).inSingletonScope();
|
||||
|
||||
// File-system extension
|
||||
bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope();
|
||||
|
||||
// Examples service@
|
||||
bind(ExamplesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExamplesServicePath)).inSingletonScope();
|
||||
|
||||
// Executable URIs known by the backend
|
||||
bind(ExecutableService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExecutableServicePath)).inSingletonScope();
|
||||
|
||||
Contribution.configure(bind, NewSketch);
|
||||
Contribution.configure(bind, OpenSketch);
|
||||
Contribution.configure(bind, CloseSketch);
|
||||
Contribution.configure(bind, SaveSketch);
|
||||
Contribution.configure(bind, SaveAsSketch);
|
||||
Contribution.configure(bind, VerifySketch);
|
||||
Contribution.configure(bind, UploadSketch);
|
||||
Contribution.configure(bind, OpenSketchExternal);
|
||||
Contribution.configure(bind, EditContributions);
|
||||
Contribution.configure(bind, QuitApp);
|
||||
Contribution.configure(bind, SketchControl);
|
||||
Contribution.configure(bind, Settings);
|
||||
Contribution.configure(bind, BurnBootloader);
|
||||
Contribution.configure(bind, BuiltInExamples);
|
||||
Contribution.configure(bind, LibraryExamples);
|
||||
Contribution.configure(bind, IncludeLibrary);
|
||||
Contribution.configure(bind, About);
|
||||
Contribution.configure(bind, Debug);
|
||||
|
||||
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
|
||||
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
|
||||
return outputService;
|
||||
});
|
||||
bind(OutputService).toService(OutputServiceImpl);
|
||||
|
||||
bind(NotificationCenter).toSelf().inSingletonScope();
|
||||
bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope();
|
||||
|
||||
// Enable the dirty indicator on uncloseable widgets.
|
||||
rebind(TabBarRendererFactory).toFactory(context => () => {
|
||||
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
|
||||
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
|
||||
const iconThemeService = context.container.get<IconThemeService>(IconThemeService);
|
||||
return new TabBarRenderer(contextMenuRenderer, decoratorService, iconThemeService);
|
||||
});
|
||||
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
|
||||
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
|
||||
bind(EditorCommandContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
|
||||
|
||||
// Silent the badge decoration in the Explorer view.
|
||||
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
|
||||
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
|
||||
|
||||
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
|
||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
||||
});
|
@@ -9,7 +9,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
* `location.hash`, the historical workspace locations, and recent sketch files.
|
||||
*
|
||||
* The following logic is used for determining the default workspace location:
|
||||
* - `hash` points to an exists in location?
|
||||
* - `hash` points to an existing location?
|
||||
* - Yes
|
||||
* - `validate location`. Is valid sketch location?
|
||||
* - Yes
|
||||
|
@@ -1,79 +0,0 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core';
|
||||
import { LabelProvider } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService } from '../common/protocol/sketches-service';
|
||||
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
|
||||
import { EditorMode } from './editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoWorkspaceService extends WorkspaceService {
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
private workspaceUri?: Promise<string | undefined>;
|
||||
|
||||
protected getDefaultWorkspaceUri(): Promise<string | undefined> {
|
||||
if (this.workspaceUri) {
|
||||
// Avoid creating a new sketch twice
|
||||
return this.workspaceUri;
|
||||
}
|
||||
this.workspaceUri = (async () => {
|
||||
try {
|
||||
const hash = window.location.hash;
|
||||
const [recentWorkspaces, recentSketches] = await Promise.all([
|
||||
this.server.getRecentWorkspaces(),
|
||||
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
|
||||
]);
|
||||
const toOpen = await new ArduinoWorkspaceRootResolver({
|
||||
isValid: this.isValid.bind(this)
|
||||
}).resolve({ hash, recentWorkspaces, recentSketches });
|
||||
if (toOpen) {
|
||||
const { uri } = toOpen;
|
||||
await this.server.setMostRecentlyUsedWorkspace(uri);
|
||||
return toOpen.uri;
|
||||
}
|
||||
const { sketchDirUri } = (await this.configService.getConfiguration());
|
||||
this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`)
|
||||
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
|
||||
} catch (err) {
|
||||
this.logger.fatal(`Failed to determine the sketch directory: ${err}`)
|
||||
this.messageService.error(
|
||||
'There was an error creating the sketch directory. ' +
|
||||
'See the log for more details. ' +
|
||||
'The application will probably not work as expected.')
|
||||
return super.getDefaultWorkspaceUri();
|
||||
}
|
||||
})();
|
||||
return this.workspaceUri;
|
||||
}
|
||||
|
||||
private async isValid(uri: string): Promise<boolean> {
|
||||
const exists = await this.fileSystem.exists(uri);
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
|
||||
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
|
||||
if (this.editorMode.proMode) {
|
||||
return true;
|
||||
}
|
||||
const sketchFolder = await this.sketchService.isSketchFolder(uri);
|
||||
return sketchFolder;
|
||||
}
|
||||
|
||||
}
|
@@ -1,13 +1,12 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { BoardsService, Board } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { InstallationProgressDialog } from '../components/progress-dialog';
|
||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
|
||||
/**
|
||||
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
|
||||
* have the corresponding core installed, it proposes the user to install the core.
|
||||
@@ -21,8 +20,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
@@ -35,9 +34,9 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
||||
const { selectedBoard } = config;
|
||||
if (selectedBoard) {
|
||||
this.boardsService.search({}).then(({ items }) => {
|
||||
const candidates = items
|
||||
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
|
||||
this.boardsService.search({}).then(packages => {
|
||||
const candidates = packages
|
||||
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
|
||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
||||
for (const candidate of candidates) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
|
@@ -4,7 +4,8 @@ import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
@@ -12,9 +13,13 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
|
||||
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
|
||||
|
||||
@@ -23,6 +28,14 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.id = 'select-board-dialog';
|
||||
this.toDispose.pushAll([
|
||||
this.onBoardConfigChangedEmitter,
|
||||
this.onFilterTextDidChangeEmitter
|
||||
]);
|
||||
}
|
||||
|
||||
search(query: string): void {
|
||||
this.onFilterTextDidChangeEmitter.fire(query);
|
||||
}
|
||||
|
||||
protected fireConfigChanged = (config: BoardsConfig.Config) => {
|
||||
@@ -36,10 +49,11 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
protected render(): React.ReactNode {
|
||||
return <div className='selectBoardContainer'>
|
||||
<BoardsConfig
|
||||
boardsService={this.boardsService}
|
||||
boardsServiceClient={this.boardsServiceClient}
|
||||
boardsServiceProvider={this.boardsServiceClient}
|
||||
notificationCenter={this.notificationCenter}
|
||||
onConfigChange={this.fireConfigChanged}
|
||||
onFocusNodeSet={this.setFocusNode} />
|
||||
onFocusNodeSet={this.setFocusNode}
|
||||
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -51,5 +65,4 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogProps extends DialogProps {
|
||||
@@ -19,8 +19,8 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected config: BoardsConfig.Config = {};
|
||||
|
||||
@@ -42,6 +42,16 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
|
||||
*/
|
||||
async open(query: string | undefined = undefined): Promise<BoardsConfig.Config | undefined> {
|
||||
if (typeof query === 'string') {
|
||||
this.widget.search(query);
|
||||
}
|
||||
return super.open();
|
||||
}
|
||||
|
||||
protected createDescription(): HTMLElement {
|
||||
const head = document.createElement('div');
|
||||
head.classList.add('head');
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { DisposableCollection } from '@theia/core';
|
||||
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Board, Port, AttachedBoardsChangeEvent, BoardWithPackage } from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
|
||||
export namespace BoardsConfig {
|
||||
|
||||
@@ -11,16 +15,18 @@ export namespace BoardsConfig {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
readonly boardsService: BoardsService;
|
||||
readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly notificationCenter: NotificationCenter;
|
||||
readonly onConfigChange: (config: Config) => void;
|
||||
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
||||
readonly onFilteredTextDidChangeEvent: Event<string>;
|
||||
}
|
||||
|
||||
export interface State extends Config {
|
||||
searchResults: Array<Board & { packageName: string }>;
|
||||
searchResults: Array<BoardWithPackage>;
|
||||
knownPorts: Port[];
|
||||
showAllPorts: boolean;
|
||||
query: string;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,11 +37,11 @@ export abstract class Item<T> extends React.Component<{
|
||||
selected: boolean,
|
||||
onClick: (item: T) => void,
|
||||
missing?: boolean,
|
||||
detail?: string
|
||||
details?: string
|
||||
}> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selected, label, missing, detail } = this.props;
|
||||
const { selected, label, missing, details } = this.props;
|
||||
const classNames = ['item'];
|
||||
if (selected) {
|
||||
classNames.push('selected');
|
||||
@@ -43,11 +49,11 @@ export abstract class Item<T> extends React.Component<{
|
||||
if (missing === true) {
|
||||
classNames.push('missing')
|
||||
}
|
||||
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
|
||||
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!details ? '' : details}`}>
|
||||
<div className='label'>
|
||||
{label}
|
||||
</div>
|
||||
{!detail ? '' : <div className='detail'>{detail}</div>}
|
||||
{!details ? '' : <div className='details'>{details}</div>}
|
||||
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
|
||||
</div>;
|
||||
}
|
||||
@@ -65,24 +71,30 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
constructor(props: BoardsConfig.Props) {
|
||||
super(props);
|
||||
|
||||
const { boardsConfig } = props.boardsServiceClient;
|
||||
const { boardsConfig } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
searchResults: [],
|
||||
knownPorts: [],
|
||||
showAllPorts: false,
|
||||
query: '',
|
||||
...boardsConfig
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateBoards();
|
||||
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
|
||||
const { boardsServiceClient: client } = this.props;
|
||||
this.updatePorts(this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty));
|
||||
this.toDispose.pushAll([
|
||||
client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
||||
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
|
||||
this.props.notificationCenter.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
|
||||
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
|
||||
})
|
||||
}),
|
||||
this.props.notificationCenter.onPlatformInstalled(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })),
|
||||
this.props.onFilteredTextDidChangeEvent(query => this.setState({ query }, () => this.updateBoards(this.state.query)))
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,15 +108,15 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
}
|
||||
|
||||
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
|
||||
const query = (typeof eventOrQuery === 'string'
|
||||
const query = typeof eventOrQuery === 'string'
|
||||
? eventOrQuery
|
||||
: eventOrQuery.target.value.toLowerCase()
|
||||
).trim();
|
||||
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
|
||||
: eventOrQuery.target.value.toLowerCase();
|
||||
this.setState({ query });
|
||||
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
|
||||
}
|
||||
|
||||
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
||||
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
|
||||
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
||||
let { selectedPort } = this.state;
|
||||
// If the currently selected port is not available anymore, unset the selected port.
|
||||
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
|
||||
@@ -114,35 +126,17 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
});
|
||||
}
|
||||
|
||||
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
|
||||
const { boardsService } = this.props;
|
||||
const query = (options.query || '').toLocaleLowerCase();
|
||||
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
|
||||
boardsService.search(options)
|
||||
.then(({ items }) => items
|
||||
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
|
||||
.sort(Board.compare))
|
||||
.then(searchResults => resolve({ searchResults }));
|
||||
});
|
||||
protected queryBoards = (options: { query?: string } = {}): Promise<Array<BoardWithPackage>> => {
|
||||
return this.props.boardsServiceProvider.searchBoards(options);
|
||||
}
|
||||
|
||||
protected get attachedBoards(): Promise<{ boards: Board[] }> {
|
||||
return this.props.boardsService.getAttachedBoards();
|
||||
protected get availablePorts(): MaybePromise<Port[]> {
|
||||
return this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty);
|
||||
}
|
||||
|
||||
protected get availablePorts(): Promise<{ ports: Port[] }> {
|
||||
return this.props.boardsService.getAvailablePorts();
|
||||
}
|
||||
|
||||
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
|
||||
return new Promise<{ knownPorts: Port[] }>(resolve => {
|
||||
availablePorts
|
||||
.then(({ ports }) => ports
|
||||
.sort(Port.compare))
|
||||
.then(knownPorts => resolve({ knownPorts }));
|
||||
});
|
||||
protected queryPorts = async (availablePorts: MaybePromise<Port[]> = this.availablePorts) => {
|
||||
const ports = await availablePorts;
|
||||
return { knownPorts: ports.sort(Port.compare) };
|
||||
}
|
||||
|
||||
protected toggleFilterPorts = () => {
|
||||
@@ -153,7 +147,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
this.setState({ selectedPort }, () => this.fireConfigChanged());
|
||||
}
|
||||
|
||||
protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => {
|
||||
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
|
||||
this.setState({ selectedBoard }, () => this.fireConfigChanged());
|
||||
}
|
||||
|
||||
@@ -183,42 +177,39 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
}
|
||||
|
||||
protected renderBoards(): React.ReactNode {
|
||||
const { selectedBoard, searchResults } = this.state;
|
||||
// Board names are not unique. We show the corresponding core name as a detail.
|
||||
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
|
||||
const distinctBoardNames = new Map<string, number>();
|
||||
for (const { name } of searchResults) {
|
||||
const counter = distinctBoardNames.get(name) || 0;
|
||||
distinctBoardNames.set(name, counter + 1);
|
||||
}
|
||||
|
||||
// Due to the non-unique board names, we have to check the package name as well.
|
||||
const selected = (board: Board & { packageName: string }) => {
|
||||
if (!!selectedBoard) {
|
||||
if (Board.equals(board, selectedBoard)) {
|
||||
if ('packageName' in selectedBoard) {
|
||||
return board.packageName === (selectedBoard as any).packageName;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const { selectedBoard, searchResults, query } = this.state;
|
||||
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
|
||||
// It is tricky when the core is not yet installed, no FQBNs are available.
|
||||
const distinctBoards = new Map<string, Board.Detailed>();
|
||||
const toKey = ({ name, packageName, fqbn }: Board.Detailed) => !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
|
||||
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
|
||||
const key = toKey(board);
|
||||
if (!distinctBoards.has(key)) {
|
||||
distinctBoards.set(key, board);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='search'>
|
||||
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
|
||||
<input
|
||||
type='search'
|
||||
value={query}
|
||||
className='theia-input'
|
||||
placeholder='SEARCH BOARD'
|
||||
onChange={this.updateBoards}
|
||||
ref={this.focusNodeSet}
|
||||
/>
|
||||
<i className='fa fa-search'></i>
|
||||
</div>
|
||||
<div className='boards list'>
|
||||
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
|
||||
{Array.from(distinctBoards.values()).map(board => <Item<BoardWithPackage>
|
||||
key={`${board.name}-${board.packageName}`}
|
||||
item={board}
|
||||
label={board.name}
|
||||
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
|
||||
selected={selected(board)}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={!Board.installed(board)}
|
||||
missing={board.missing}
|
||||
/>)}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
@@ -266,9 +257,9 @@ export namespace BoardsConfig {
|
||||
|
||||
export namespace Config {
|
||||
|
||||
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
|
||||
export function sameAs(config: Config, other: Config | Board): boolean {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
if (AttachedSerialBoard.is(other)) {
|
||||
if (Board.is(other)) {
|
||||
return !!selectedBoard
|
||||
&& Board.equals(other, selectedBoard)
|
||||
&& Port.sameAs(selectedPort, other.port);
|
||||
|
@@ -0,0 +1,117 @@
|
||||
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 { 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';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
|
||||
this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
|
||||
}
|
||||
|
||||
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDisposeOnBoardChange.dispose();
|
||||
this.mainMenuManager.update();
|
||||
if (selectedBoard) {
|
||||
const { fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
|
||||
if (configOptions.length) {
|
||||
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z01_boardsConfig']; // `z_` is for ordering.
|
||||
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
|
||||
const menuPath = [...boardsConfigMenuPath, `${option}`];
|
||||
const commands = new Map<string, Disposable & { label: string }>()
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
|
||||
isToggled: () => value.selected
|
||||
};
|
||||
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
|
||||
}
|
||||
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
|
||||
...Array.from(commands.keys()).map((commandId, i) => {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
|
||||
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (programmers.length) {
|
||||
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)));
|
||||
for (const programmer of programmers) {
|
||||
const { id, name } = programmer;
|
||||
const command = { id: `${fqbn}-programmer--${id}` };
|
||||
const handler = {
|
||||
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
|
||||
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
|
||||
};
|
||||
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
|
||||
]);
|
||||
}
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
211
arduino-ide-extension/src/browser/boards/boards-data-store.ts
Normal file
211
arduino-ide-extension/src/browser/boards/boards-data-store.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { notEmpty } from '../../common/utils';
|
||||
import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('store')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
|
||||
const { installedVersion: version } = item;
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
let shouldFireChanged = false;
|
||||
for (const fqbn of item.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
|
||||
if (!data || !data.length) {
|
||||
const details = await this.getBoardDetailsSafe(fqbn);
|
||||
if (details) {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
shouldFireChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
async appendConfigToFqbn(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string | undefined> {
|
||||
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
|
||||
return ConfigOption.decorate(fqbn, configOptions);
|
||||
}
|
||||
|
||||
async getData(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
|
||||
|
||||
if (!fqbn) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
|
||||
if (BoardsDataStore.Data.is(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const boardDetails = await this.getBoardDetailsSafe(fqbn);
|
||||
if (!boardDetails) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
data = { configOptions: boardDetails.configOptions, programmers: boardDetails.programmers };
|
||||
await this.storageService.setData(key, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async selectProgrammer(
|
||||
{ fqbn, selectedProgrammer }: { fqbn: string, selectedProgrammer: Programmer },
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
||||
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
const { programmers } = data;
|
||||
if (!programmers.find(p => Programmer.equals(selectedProgrammer, p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({ fqbn, data: { ...data, selectedProgrammer }, version });
|
||||
this.fireChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
async selectConfigOption(
|
||||
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
||||
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
const { configOptions } = data;
|
||||
const configOption = configOptions.find(c => c.option === option);
|
||||
if (!configOption) {
|
||||
return false;
|
||||
}
|
||||
let updated = false;
|
||||
for (const value of configOption.values) {
|
||||
if (value.value === selectedValue) {
|
||||
(value as any).selected = true;
|
||||
updated = true;
|
||||
} else {
|
||||
(value as any).selected = false;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({ fqbn, data, version });
|
||||
this.fireChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async setData(
|
||||
{ fqbn, data, version }: { fqbn: string, data: BoardsDataStore.Data, version: Installable.Version }): Promise<void> {
|
||||
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
return this.storageService.setData(key, data);
|
||||
}
|
||||
|
||||
protected getStorageKey(fqbn: string, version: Installable.Version): string {
|
||||
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
|
||||
}
|
||||
|
||||
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
|
||||
try {
|
||||
const details = this.boardsService.getBoardDetails({ fqbn });
|
||||
return details;
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('loading board data') && err.message.includes('is not installed')) {
|
||||
this.logger.warn(`The boards package is not installed for board with FQBN: ${fqbn}`);
|
||||
} else {
|
||||
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(): void {
|
||||
this.onChangedEmitter.fire();
|
||||
}
|
||||
|
||||
protected async getBoardsPackageVersion(fqbn: string | undefined): Promise<Installable.Version | undefined> {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const boardsPackage = await this.boardsService.getContainerBoardPackage({ fqbn });
|
||||
if (!boardsPackage) {
|
||||
return undefined;
|
||||
}
|
||||
return boardsPackage.installedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BoardsDataStore {
|
||||
export interface Data {
|
||||
readonly configOptions: ConfigOption[];
|
||||
readonly programmers: Programmer[];
|
||||
readonly selectedProgrammer?: Programmer;
|
||||
}
|
||||
export namespace Data {
|
||||
export const EMPTY: Data = {
|
||||
configOptions: [],
|
||||
programmers: []
|
||||
};
|
||||
export function is(arg: any): arg is Data {
|
||||
return !!arg
|
||||
&& 'configOptions' in arg && Array.isArray(arg['configOptions'])
|
||||
&& 'programmers' in arg && Array.isArray(arg['programmers'])
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../components/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = 'Boards Manager';
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
|
||||
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@@ -19,9 +19,18 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
iconClass: 'fa fa-microchip',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardPackage) => item.name,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemRenderer
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformInstalled(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onPlatformUninstalled(() => this.refresh(undefined)),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,178 +0,0 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected storageService: LocalStorageService;
|
||||
|
||||
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
|
||||
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
|
||||
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
|
||||
/**
|
||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
|
||||
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
|
||||
* We have to listen on such changes and auto-reconnect the same board on another port.
|
||||
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
|
||||
*/
|
||||
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
|
||||
protected _boardsConfig: BoardsConfig.Config = {};
|
||||
|
||||
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
|
||||
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
|
||||
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
|
||||
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
return this.loadState();
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
|
||||
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
const { selectedPort, selectedBoard } = this.boardsConfig;
|
||||
this.onAttachedBoardsChangedEmitter.fire(event);
|
||||
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
|
||||
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
|
||||
this.boardsConfig = {
|
||||
selectedBoard,
|
||||
selectedPort: undefined
|
||||
};
|
||||
}
|
||||
// Try to reconnect.
|
||||
this.tryReconnect(attached.boards, attached.ports);
|
||||
}
|
||||
|
||||
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
|
||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
|
||||
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
|
||||
|
||||
this.boardsConfig = this.latestValidBoardsConfig;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
|
||||
// See documentation on `latestValidBoardsConfig`.
|
||||
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
|
||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
|
||||
|
||||
this.boardsConfig = {
|
||||
...this.latestValidBoardsConfig,
|
||||
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
notifyBoardInstalled(event: BoardInstalledEvent): void {
|
||||
this.logger.info('Board installed: ', JSON.stringify(event));
|
||||
this.onBoardInstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
|
||||
this.logger.info('Board uninstalled: ', JSON.stringify(event));
|
||||
this.onBoardUninstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
set boardsConfig(config: BoardsConfig.Config) {
|
||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
||||
this._boardsConfig = config;
|
||||
if (this.canUploadTo(this._boardsConfig)) {
|
||||
this.latestValidBoardsConfig = this._boardsConfig;
|
||||
}
|
||||
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
|
||||
}
|
||||
|
||||
get boardsConfig(): BoardsConfig.Config {
|
||||
return this._boardsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
|
||||
*/
|
||||
canVerify(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
|
||||
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
|
||||
*/
|
||||
canUploadTo(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
|
||||
|
||||
if (!this.canVerify(config, options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { name } = config.selectedBoard;
|
||||
if (!config.selectedPort) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard.fqbn) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected saveState(): Promise<void> {
|
||||
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
|
||||
}
|
||||
|
||||
protected async loadState(): Promise<void> {
|
||||
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
|
||||
if (storedValidBoardsConfig) {
|
||||
this.latestValidBoardsConfig = storedValidBoardsConfig;
|
||||
if (this.canUploadTo(this.latestValidBoardsConfig)) {
|
||||
this.boardsConfig = this.latestValidBoardsConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,476 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
import {
|
||||
Port,
|
||||
Board,
|
||||
BoardsService,
|
||||
BoardsPackage,
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardWithPackage
|
||||
} from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { naturalCompare } from '../../common/utils';
|
||||
import { compareAnything } from '../theia/monaco/comparers';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { CommandService } from '@theia/core';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
|
||||
interface BoardMatch {
|
||||
readonly board: BoardWithPackage;
|
||||
readonly matches: monaco.filters.IMatch[] | undefined;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(StorageService)
|
||||
protected storageService: StorageService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected boardsService: BoardsService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected commandService: CommandService;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
|
||||
|
||||
/**
|
||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
|
||||
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
|
||||
* We have to listen on such changes and auto-reconnect the same board on another port.
|
||||
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
|
||||
*/
|
||||
protected latestValidBoardsConfig: RecursiveRequired<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[] = [];
|
||||
protected _availableBoards: AvailableBoard[] = [];
|
||||
|
||||
/**
|
||||
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
|
||||
* This even also fires, when the boards package was not available for the currently selected board,
|
||||
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
||||
* currently selected board.\
|
||||
* This even also emitted when the board package for the currently selected board was uninstalled.
|
||||
*/
|
||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onAttachedBoardsChanged(this.notifyAttachedBoardsChanged.bind(this));
|
||||
this.notificationCenter.onPlatformInstalled(this.notifyPlatformInstalled.bind(this));
|
||||
this.notificationCenter.onPlatformUninstalled(this.notifyPlatformUninstalled.bind(this));
|
||||
|
||||
Promise.all([
|
||||
this.boardsService.getAttachedBoards(),
|
||||
this.boardsService.getAvailablePorts(),
|
||||
this.loadState()
|
||||
]).then(([attachedBoards, availablePorts]) => {
|
||||
this._attachedBoards = attachedBoards;
|
||||
this._availablePorts = availablePorts;
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
});
|
||||
}
|
||||
|
||||
protected notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
if (!AttachedBoardsChangeEvent.isEmpty(event)) {
|
||||
this.logger.info('Attached boards and available ports changed:');
|
||||
this.logger.info(AttachedBoardsChangeEvent.toString(event));
|
||||
this.logger.info(`------------------------------------------`);
|
||||
}
|
||||
this._attachedBoards = event.newState.boards;
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
}
|
||||
|
||||
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
this.logger.info('Boards package installed: ', JSON.stringify(event));
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
const { installedVersion, id } = event.item;
|
||||
if (selectedBoard) {
|
||||
const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
|
||||
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
|
||||
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
|
||||
this.boardsConfig = {
|
||||
...this.boardsConfig,
|
||||
selectedBoard: installedBoard
|
||||
};
|
||||
return;
|
||||
}
|
||||
// The board name can change after install.
|
||||
// This logic handles it "gracefully" by unselecting the board, so that we can avoid no FQBN is set error.
|
||||
// https://github.com/arduino/arduino-cli/issues/620
|
||||
// https://github.com/arduino/arduino-pro-ide/issues/374
|
||||
if (BoardWithPackage.is(selectedBoard) && selectedBoard.packageId === event.item.id && !installedBoard) {
|
||||
this.messageService.warn(`Could not find previously selected board '${selectedBoard.name}' in installed platform '${event.item.name}'. Please manually reselect the board you want to use. Do you want to reselect it now?`, 'Reselect later', 'Yes').then(async answer => {
|
||||
if (answer === 'Yes') {
|
||||
this.commandService.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id, selectedBoard.name);
|
||||
}
|
||||
});
|
||||
this.boardsConfig = {}
|
||||
return;
|
||||
}
|
||||
// Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954
|
||||
// E.g: install `adafruit:avr`, then select `adafruit:avr:adafruit32u4` board, and finally install the required `arduino:avr`
|
||||
this.boardsConfig = this.boardsConfig;
|
||||
}
|
||||
}
|
||||
|
||||
protected notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
|
||||
this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
if (selectedBoard && selectedBoard.fqbn) {
|
||||
const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
|
||||
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
|
||||
this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
|
||||
const selectedBoardWithoutFqbn = {
|
||||
name: selectedBoard.name
|
||||
// No FQBN
|
||||
};
|
||||
this.boardsConfig = {
|
||||
...this.boardsConfig,
|
||||
selectedBoard: selectedBoardWithoutFqbn
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async tryReconnect(): Promise<boolean> {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
|
||||
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
|
||||
|
||||
this.boardsConfig = this.latestValidBoardsConfig;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
|
||||
// See documentation on `latestValidBoardsConfig`.
|
||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
|
||||
|
||||
this.boardsConfig = {
|
||||
...this.latestValidBoardsConfig,
|
||||
selectedPort: board.port
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
set boardsConfig(config: BoardsConfig.Config) {
|
||||
this.doSetBoardsConfig(config);
|
||||
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
|
||||
}
|
||||
|
||||
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
|
||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
||||
this._boardsConfig = config;
|
||||
if (this.canUploadTo(this._boardsConfig)) {
|
||||
this.latestValidBoardsConfig = this._boardsConfig;
|
||||
}
|
||||
}
|
||||
|
||||
async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise<Array<BoardWithPackage>> {
|
||||
const boards = await this.boardsService.allBoards({});
|
||||
const coresFilter = !!cores && cores.length
|
||||
? ((toFilter: BoardWithPackage) => cores.some(core => core === toFilter.packageName || core === toFilter.packageId))
|
||||
: () => true;
|
||||
if (!query) {
|
||||
return boards.filter(coresFilter).sort(Board.compare);
|
||||
}
|
||||
const toMatch = ((toFilter: BoardWithPackage) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) })));
|
||||
const compareEntries = (left: BoardMatch, right: BoardMatch, lookFor: string) => {
|
||||
const leftMatches = left.matches || [];
|
||||
const rightMatches = right.matches || [];
|
||||
if (leftMatches.length && !rightMatches.length) {
|
||||
return -1;
|
||||
}
|
||||
if (!leftMatches.length && rightMatches.length) {
|
||||
return 1;
|
||||
}
|
||||
if (leftMatches.length === 0 && rightMatches.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const leftLabel = left.board.name.replace(/\r?\n/g, ' ');
|
||||
const rightLabel = right.board.name.replace(/\r?\n/g, ' ');
|
||||
return compareAnything(leftLabel, rightLabel, lookFor);
|
||||
}
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
return boards
|
||||
.filter(coresFilter)
|
||||
.map(toMatch)
|
||||
.filter(({ matches }) => !!matches)
|
||||
.sort((left, right) => compareEntries(left, right, normalizedQuery))
|
||||
.map(({ board }) => board);
|
||||
}
|
||||
|
||||
get boardsConfig(): BoardsConfig.Config {
|
||||
return this._boardsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
|
||||
*/
|
||||
canVerify(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
|
||||
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if `canVerify`, the board has an FQBN and the `config.selectedPort` is also set, hence can upload to board. Otherwise, `false`.
|
||||
*/
|
||||
canUploadTo(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
|
||||
|
||||
if (!this.canVerify(config, options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { name } = config.selectedBoard;
|
||||
if (!config.selectedPort) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard.fqbn) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get availableBoards(): AvailableBoard[] {
|
||||
return this._availableBoards;
|
||||
}
|
||||
|
||||
async waitUntilAvailable(what: Board & { port: Port }, timeout?: number): Promise<void> {
|
||||
const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) =>
|
||||
haystack.find(board => Board.equals(needle, board) && Port.equals(needle.port, board.port));
|
||||
const timeoutTask = !!timeout && timeout > 0
|
||||
? new Promise<void>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout} ms.`)), timeout))
|
||||
: new Promise<void>(() => { /* never */ });
|
||||
const waitUntilTask = new Promise<void>(resolve => {
|
||||
let candidate = find(what, this.availableBoards);
|
||||
if (candidate) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const disposable = this.onAvailableBoardsChanged(availableBoards => {
|
||||
candidate = find(what, availableBoards);
|
||||
if (candidate) {
|
||||
disposable.dispose();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
return await Promise.race([waitUntilTask, timeoutTask]);
|
||||
}
|
||||
|
||||
protected async reconcileAvailableBoards(): Promise<void> {
|
||||
const attachedBoards = this._attachedBoards;
|
||||
const availablePorts = this._availablePorts;
|
||||
// Unset the port on the user's config, if it is not available anymore.
|
||||
if (this.boardsConfig.selectedPort && !availablePorts.some(port => Port.sameAs(port, this.boardsConfig.selectedPort))) {
|
||||
this.doSetBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined });
|
||||
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
|
||||
}
|
||||
const boardsConfig = this.boardsConfig;
|
||||
const currentAvailableBoards = this._availableBoards;
|
||||
const availableBoards: AvailableBoard[] = [];
|
||||
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
|
||||
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
|
||||
|
||||
for (const boardPort of availableBoardPorts) {
|
||||
let state = AvailableBoard.State.incomplete; // Initial pessimism.
|
||||
let board = attachedSerialBoards.find(({ port }) => Port.sameAs(boardPort, port));
|
||||
if (board) {
|
||||
state = AvailableBoard.State.recognized;
|
||||
} else {
|
||||
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
|
||||
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
|
||||
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
|
||||
if (lastSelectedBoard) {
|
||||
board = {
|
||||
...lastSelectedBoard,
|
||||
port: boardPort
|
||||
};
|
||||
state = AvailableBoard.State.guessed;
|
||||
}
|
||||
}
|
||||
if (!board) {
|
||||
availableBoards.push({ name: 'Unknown', port: boardPort, state });
|
||||
} else {
|
||||
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
|
||||
availableBoards.push({ ...board, state, selected, port: boardPort });
|
||||
}
|
||||
}
|
||||
|
||||
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
|
||||
availableBoards.push({
|
||||
...boardsConfig.selectedBoard,
|
||||
port: boardsConfig.selectedPort,
|
||||
selected: true,
|
||||
state: AvailableBoard.State.incomplete
|
||||
});
|
||||
}
|
||||
|
||||
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.compare);
|
||||
let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length;
|
||||
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
|
||||
hasChanged = AvailableBoard.compare(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0;
|
||||
}
|
||||
if (hasChanged) {
|
||||
this._availableBoards = sortedAvailableBoards;
|
||||
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getLastSelectedBoardOnPort(port: Port | string | undefined): Promise<Board | undefined> {
|
||||
if (!port) {
|
||||
return undefined;
|
||||
}
|
||||
const key = this.getLastSelectedBoardOnPortKey(port);
|
||||
return this.storageService.getData<Board>(key);
|
||||
}
|
||||
|
||||
protected async saveState(): Promise<void> {
|
||||
// We save the port with the selected board name/FQBN, to be able to guess a better board name.
|
||||
// Required when the attached board belongs to a 3rd party boards package, and neither the name, nor
|
||||
// the FQBN can be retrieved with a `board list` command.
|
||||
// https://github.com/arduino/arduino-cli/issues/623
|
||||
const { selectedBoard, selectedPort } = this.boardsConfig;
|
||||
if (selectedBoard && selectedPort) {
|
||||
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
|
||||
await this.storageService.setData(key, selectedBoard);
|
||||
}
|
||||
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
|
||||
}
|
||||
|
||||
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
|
||||
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
|
||||
return `last-selected-board-on-port:${typeof port === 'string' ? port : Port.toString(port)}`;
|
||||
}
|
||||
|
||||
protected async loadState(): Promise<void> {
|
||||
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
|
||||
if (storedValidBoardsConfig) {
|
||||
this.latestValidBoardsConfig = storedValidBoardsConfig;
|
||||
if (this.canUploadTo(this.latestValidBoardsConfig)) {
|
||||
this.boardsConfig = this.latestValidBoardsConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
|
||||
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
|
||||
* If it has the selected board and a associated port, it can be used for `upload`. We render an available board for the user
|
||||
* when it has the `port` set.
|
||||
*/
|
||||
export interface AvailableBoard extends Board {
|
||||
readonly state: AvailableBoard.State;
|
||||
readonly selected?: boolean;
|
||||
readonly port?: Port;
|
||||
}
|
||||
|
||||
export namespace AvailableBoard {
|
||||
|
||||
export enum State {
|
||||
/**
|
||||
* Retrieved from the CLI via the `board list` command.
|
||||
*/
|
||||
'recognized',
|
||||
/**
|
||||
* Guessed the name/FQBN of the board from the available board ports (3rd party).
|
||||
*/
|
||||
'guessed',
|
||||
/**
|
||||
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
|
||||
*/
|
||||
'incomplete'
|
||||
}
|
||||
|
||||
export function is(board: any): board is AvailableBoard {
|
||||
return Board.is(board) && 'state' in board;
|
||||
}
|
||||
|
||||
export function hasPort(board: AvailableBoard): board is AvailableBoard & { port: Port } {
|
||||
return !!board.port;
|
||||
}
|
||||
|
||||
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
|
||||
if (left.selected && !right.selected) {
|
||||
return -1;
|
||||
}
|
||||
if (right.selected && !left.selected) {
|
||||
return 1;
|
||||
}
|
||||
let result = naturalCompare(left.name, right.name);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
if (left.fqbn && right.fqbn) {
|
||||
result = naturalCompare(left.fqbn, right.fqbn);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (left.port && right.port) {
|
||||
result = Port.compare(left.port, right.port);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!!left.selected && !right.selected) {
|
||||
return -1;
|
||||
}
|
||||
if (!!right.selected && !left.selected) {
|
||||
return 1;
|
||||
}
|
||||
return left.state - right.state;
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { CommandRegistry, DisposableCollection } from '@theia/core';
|
||||
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Port } from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { BoardsServiceProvider, AvailableBoard } from './boards-service-provider';
|
||||
|
||||
export interface BoardsDropDownListCoords {
|
||||
readonly top: number;
|
||||
@@ -16,14 +17,9 @@ export interface BoardsDropDownListCoords {
|
||||
export namespace BoardsDropDown {
|
||||
export interface Props {
|
||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||
readonly items: Item[];
|
||||
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
|
||||
readonly openBoardsConfig: () => void;
|
||||
}
|
||||
export interface Item {
|
||||
readonly label: string;
|
||||
readonly selected: boolean;
|
||||
readonly onClick: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
@@ -51,48 +47,30 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
if (coords === 'hidden') {
|
||||
return '';
|
||||
}
|
||||
items.push({
|
||||
label: 'Select Other Board & Port',
|
||||
selected: false,
|
||||
onClick: () => this.props.openBoardsConfig()
|
||||
})
|
||||
return <div className='arduino-boards-dropdown-list'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...coords
|
||||
}}>
|
||||
{items.map(this.renderItem)}
|
||||
{this.renderItem({
|
||||
label: 'Select Other Board & Port',
|
||||
onClick: () => this.props.openBoardsConfig()
|
||||
})}
|
||||
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
|
||||
</div>
|
||||
}
|
||||
|
||||
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
|
||||
const { label, selected, onClick } = item;
|
||||
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
|
||||
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
|
||||
<div>
|
||||
{label}
|
||||
</div>
|
||||
{selected ? <span className='fa fa-check'/> : ''}
|
||||
{selected ? <span className='fa fa-check' /> : ''}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BoardsToolBarItem {
|
||||
|
||||
export interface Props {
|
||||
readonly boardService: BoardsService;
|
||||
readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
boardsConfig: BoardsConfig.Config;
|
||||
attachedBoards: Board[];
|
||||
availablePorts: Port[];
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
|
||||
|
||||
static TOOLBAR_ID: 'boards-toolbar';
|
||||
@@ -102,10 +80,9 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
constructor(props: BoardsToolBarItem.Props) {
|
||||
super(props);
|
||||
|
||||
const { availableBoards } = props.boardsServiceClient;
|
||||
this.state = {
|
||||
boardsConfig: this.props.boardsServiceClient.boardsConfig,
|
||||
attachedBoards: [],
|
||||
availablePorts: [],
|
||||
availableBoards,
|
||||
coords: 'hidden'
|
||||
};
|
||||
|
||||
@@ -115,17 +92,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { boardsServiceClient: client, boardService } = this.props;
|
||||
this.toDispose.pushAll([
|
||||
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
|
||||
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
|
||||
]);
|
||||
Promise.all([
|
||||
boardService.getAttachedBoards(),
|
||||
boardService.getAvailablePorts()
|
||||
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
|
||||
this.setState({ attachedBoards, availablePorts })
|
||||
});
|
||||
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
@@ -146,7 +113,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ coords: 'hidden'});
|
||||
this.setState({ coords: 'hidden' });
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
@@ -154,41 +121,52 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
|
||||
const { coords, availableBoards } = this.state;
|
||||
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
|
||||
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
|
||||
const configuredBoard = attachedBoards
|
||||
.filter(AttachedSerialBoard.is)
|
||||
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
|
||||
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
|
||||
|
||||
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
|
||||
label: `${board.name} at ${board.port}`,
|
||||
selected: configuredBoard === board,
|
||||
onClick: () => {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
|
||||
}
|
||||
const decorator = (() => {
|
||||
const selectedBoard = availableBoards.find(({ selected }) => selected);
|
||||
if (!selectedBoard || !selectedBoard.port) {
|
||||
return 'fa fa-times notAttached'
|
||||
}
|
||||
}));
|
||||
if (selectedBoard.state === AvailableBoard.State.guessed) {
|
||||
return 'fa fa-exclamation-triangle guessed'
|
||||
}
|
||||
return ''
|
||||
})();
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='arduino-boards-toolbar-item-container'>
|
||||
<div className='arduino-boards-toolbar-item' title={title}>
|
||||
<div className='inner-container' onClick={this.show}>
|
||||
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
|
||||
<span className={decorator} />
|
||||
<div className='label noWrapInfo'>
|
||||
<div className='noWrapInfo noselect'>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<span className='fa fa-caret-down caret'/>
|
||||
<span className='fa fa-caret-down caret' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
coords={coords}
|
||||
items={items}
|
||||
items={availableBoards.filter(AvailableBoard.hasPort).map(board => ({
|
||||
...board,
|
||||
onClick: () => {
|
||||
if (board.state === AvailableBoard.State.incomplete) {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedPort: board.port
|
||||
};
|
||||
this.openDialog();
|
||||
} else {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: board.port
|
||||
}
|
||||
}
|
||||
}
|
||||
}))}
|
||||
openBoardsConfig={this.openDialog}>
|
||||
</BoardsDropDown>
|
||||
</React.Fragment>;
|
||||
@@ -200,3 +178,16 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
};
|
||||
|
||||
}
|
||||
export namespace BoardsToolBarItem {
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceClient: BoardsServiceProvider;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
availableBoards: AvailableBoard[];
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import { ArduinoMenus } from '../arduino-frontend-contribution';
|
||||
import { BoardPackage } from '../../common/protocol/boards-service';
|
||||
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
|
||||
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<BoardPackage> {
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||
|
||||
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
|
||||
|
||||
@@ -19,7 +19,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
rank: 600
|
||||
},
|
||||
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
|
||||
toggleKeybinding: 'ctrlcmd+shift+b'
|
||||
toggleKeybinding: 'CtrlCmd+Shift+B'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS, {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: this.toggleCommand.id,
|
||||
label: 'Boards Manager...'
|
||||
label: 'Boards Manager...',
|
||||
order: '4'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,16 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { QuickOpenContribution } from '@theia/core/lib/browser/quick-open';
|
||||
import { KeybindingContribution } from '@theia/core/lib/browser/keybinding';
|
||||
import { BoardsQuickOpenService } from './boards-quick-open-service';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(BoardsQuickOpenService).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(BoardsQuickOpenService);
|
||||
bind(KeybindingContribution).toService(BoardsQuickOpenService);
|
||||
bind(QuickOpenContribution).toService(BoardsQuickOpenService);
|
||||
bind(ILogger).toDynamicValue(({ container }) => container.get<ILogger>(ILogger).child('boards-quick-open'))
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('boards-quick-open');
|
||||
});
|
@@ -0,0 +1,309 @@
|
||||
import * as fuzzy from 'fuzzy';
|
||||
import { inject, injectable, postConstruct, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
|
||||
import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { QuickOpenItem, QuickOpenModel, QuickOpenMode, QuickOpenGroupItem } from '@theia/core/lib/common/quick-open-model';
|
||||
import {
|
||||
QuickOpenService,
|
||||
QuickOpenHandler,
|
||||
QuickOpenOptions,
|
||||
QuickOpenItemOptions,
|
||||
QuickOpenContribution,
|
||||
QuickOpenActionProvider,
|
||||
QuickOpenHandlerRegistry,
|
||||
QuickOpenGroupItemOptions
|
||||
} from '@theia/core/lib/browser/quick-open';
|
||||
import { naturalCompare } from '../../../common/utils';
|
||||
import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol';
|
||||
import { BoardsDataStore } from '../boards-data-store';
|
||||
import { BoardsServiceProvider, AvailableBoard } from '../boards-service-provider';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command {
|
||||
|
||||
readonly id = 'arduino-boards-quick-open';
|
||||
readonly prefix = '|';
|
||||
readonly description = 'Configure Available Boards';
|
||||
readonly label: 'Configure Available Boards';
|
||||
|
||||
@inject(ILogger)
|
||||
@named('boards-quick-open')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(QuickOpenService)
|
||||
protected readonly quickOpenService: QuickOpenService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected notificationCenter: NotificationCenter;
|
||||
|
||||
protected isOpen: boolean = false;
|
||||
protected currentQuery: string = '';
|
||||
// Attached boards plus the user's config.
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
// Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional.
|
||||
protected data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY;
|
||||
protected allBoards: Board.Detailed[] = []
|
||||
protected selectedBoard?: (AvailableBoard & { port: Port });
|
||||
|
||||
// `init` name is used by the `QuickOpenHandler`.
|
||||
@postConstruct()
|
||||
protected postConstruct(): void {
|
||||
this.notificationCenter.onIndexUpdated(() => this.update(this.availableBoards));
|
||||
this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards));
|
||||
this.update(this.boardsServiceClient.availableBoards);
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(this, { execute: () => this.open() });
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({ command: this.id, keybinding: 'ctrlCmd+k ctrlCmd+b' });
|
||||
}
|
||||
|
||||
registerQuickOpenHandlers(registry: QuickOpenHandlerRegistry): void {
|
||||
registry.registerHandler(this);
|
||||
}
|
||||
|
||||
getModel(): QuickOpenModel {
|
||||
return this;
|
||||
}
|
||||
|
||||
getOptions(): QuickOpenOptions {
|
||||
let placeholder = '';
|
||||
if (!this.selectedBoard) {
|
||||
placeholder += 'No board selected.';
|
||||
}
|
||||
placeholder += 'Type to filter boards';
|
||||
if (this.data.configOptions.length) {
|
||||
placeholder += ' or use the ↓↑ keys to adjust the board settings...';
|
||||
} else {
|
||||
placeholder += '...';
|
||||
}
|
||||
return {
|
||||
placeholder,
|
||||
fuzzyMatchLabel: true,
|
||||
onClose: () => this.isOpen = false
|
||||
};
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.isOpen = true;
|
||||
this.quickOpenService.open(this, this.getOptions());
|
||||
}
|
||||
|
||||
onType(
|
||||
lookFor: string,
|
||||
acceptor: (items: QuickOpenItem<QuickOpenItemOptions>[], actionProvider?: QuickOpenActionProvider) => void): void {
|
||||
|
||||
this.currentQuery = lookFor;
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
const availableBoards = this.availableBoards.filter(AvailableBoard.hasPort).filter(({ name }) => fuzzyFilter(name));
|
||||
const toAccept: QuickOpenItem<QuickOpenItemOptions>[] = [];
|
||||
|
||||
// Show the selected attached in a different group.
|
||||
if (this.selectedBoard && fuzzyFilter(this.selectedBoard.name)) {
|
||||
toAccept.push(this.toQuickItem(this.selectedBoard, { groupLabel: 'Selected Board' }));
|
||||
}
|
||||
|
||||
// Filter the selected from the attached ones.
|
||||
toAccept.push(...availableBoards.filter(board => board !== this.selectedBoard).map((board, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
// If no `selectedBoard`, then this item is the top one, no borders required.
|
||||
group = { groupLabel: 'Attached Boards', showBorder: !!this.selectedBoard };
|
||||
}
|
||||
return this.toQuickItem(board, group);
|
||||
}));
|
||||
|
||||
// Show the config only if the `input` is empty.
|
||||
if (!lookFor.trim().length) {
|
||||
toAccept.push(...this.data.configOptions.map((config, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
group = { groupLabel: 'Board Settings', showBorder: true };
|
||||
}
|
||||
return this.toQuickItem(config, group);
|
||||
}));
|
||||
} else {
|
||||
toAccept.push(...this.allBoards.filter(({ name }) => fuzzyFilter(name)).map((board, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
group = { groupLabel: 'Boards', showBorder: true };
|
||||
}
|
||||
return this.toQuickItem(board, group);
|
||||
}));
|
||||
}
|
||||
|
||||
acceptor(toAccept);
|
||||
}
|
||||
|
||||
private fuzzyFilter(lookFor: string): (inputString: string) => boolean {
|
||||
const shouldFilter = !!lookFor.trim().length;
|
||||
return (inputString: string) => shouldFilter ? fuzzy.test(lookFor.toLocaleLowerCase(), inputString.toLocaleLowerCase()) : true;
|
||||
}
|
||||
|
||||
protected async update(availableBoards: AvailableBoard[]): Promise<void> {
|
||||
// `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!)
|
||||
const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected);
|
||||
const [data, boards] = await Promise.all([
|
||||
selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY),
|
||||
this.boardsService.allBoards({})
|
||||
]);
|
||||
this.allBoards = Board.decorateBoards(selectedBoard, boards)
|
||||
.filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board)));
|
||||
this.availableBoards = availableBoards;
|
||||
this.data = data;
|
||||
this.selectedBoard = selectedBoard;
|
||||
|
||||
if (this.isOpen) {
|
||||
// Hack, to update the state without closing and reopening the quick open widget.
|
||||
(this.quickOpenService as any).onType(this.currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
protected toQuickItem(item: BoardsQuickOpenService.Item, group?: QuickOpenGroupItemOptions): QuickOpenItem<QuickOpenItemOptions> {
|
||||
let options: QuickOpenItemOptions;
|
||||
if (AvailableBoard.is(item)) {
|
||||
const description = `on ${Port.toString(item.port)}`
|
||||
options = {
|
||||
label: `${item.name}`,
|
||||
description,
|
||||
descriptionHighlights: [
|
||||
{
|
||||
start: 0,
|
||||
end: description.length
|
||||
}
|
||||
],
|
||||
run: this.toRun(() => this.boardsServiceClient.boardsConfig = ({ selectedBoard: item, selectedPort: item.port }))
|
||||
};
|
||||
} else if (ConfigOption.is(item)) {
|
||||
const selected = item.values.find(({ selected }) => selected);
|
||||
const value = selected ? selected.label : 'Not set';
|
||||
const label = `${item.label}: ${value}`;
|
||||
options = {
|
||||
label,
|
||||
// Intended to match the value part of a board setting.
|
||||
// NOTE: this does not work, as `fuzzyMatchLabel: true` is set. Manual highlighting is ignored, apparently.
|
||||
labelHighlights: [
|
||||
{
|
||||
start: label.length - value.length,
|
||||
end: label.length
|
||||
}
|
||||
],
|
||||
run: (mode) => {
|
||||
if (mode === QuickOpenMode.OPEN) {
|
||||
this.setConfig(item);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (!selected) {
|
||||
options.description = 'Not set';
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
label: `${item.name}`,
|
||||
description: `${item.missing ? '' : `[installed with '${item.packageName}']`}`,
|
||||
run: (mode) => {
|
||||
if (mode === QuickOpenMode.OPEN) {
|
||||
this.selectBoard(item);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (group) {
|
||||
return new QuickOpenGroupItem<QuickOpenGroupItemOptions>({ ...options, ...group });
|
||||
} else {
|
||||
return new QuickOpenItem<QuickOpenItemOptions>(options);
|
||||
}
|
||||
}
|
||||
|
||||
protected toRun(run: (() => void)): ((mode: QuickOpenMode) => boolean) {
|
||||
return (mode) => {
|
||||
if (mode !== QuickOpenMode.OPEN) {
|
||||
return false;
|
||||
}
|
||||
run();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
protected async selectBoard(board: Board): Promise<void> {
|
||||
const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare);
|
||||
const toItem = (port: Port) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
label: Port.toString(port, { useLabel: true }),
|
||||
run: this.toRun(() => {
|
||||
this.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: port
|
||||
};
|
||||
})
|
||||
});
|
||||
const options = {
|
||||
placeholder: `Select a port for '${board.name}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
|
||||
fuzzyMatchLabel: true
|
||||
}
|
||||
this.quickOpenService.open({
|
||||
onType: (lookFor, acceptor) => {
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
acceptor(allPorts.filter(({ address }) => fuzzyFilter(address)).map(toItem));
|
||||
}
|
||||
}, options);
|
||||
}
|
||||
|
||||
protected async setConfig(config: ConfigOption): Promise<void> {
|
||||
const toItem = (value: ConfigValue) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
label: value.label,
|
||||
iconClass: value.selected ? 'fa fa-check' : '',
|
||||
run: this.toRun(() => {
|
||||
if (!this.selectedBoard) {
|
||||
this.logger.warn(`Could not alter the boards settings. No board selected. ${JSON.stringify(config)}`);
|
||||
return;
|
||||
}
|
||||
if (!this.selectedBoard.fqbn) {
|
||||
this.logger.warn(`Could not alter the boards settings. The selected board does not have a FQBN. ${JSON.stringify(this.selectedBoard)}`);
|
||||
return;
|
||||
}
|
||||
const { fqbn } = this.selectedBoard;
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option: config.option,
|
||||
selectedValue: value.value
|
||||
});
|
||||
})
|
||||
});
|
||||
const options = {
|
||||
placeholder: `Configure '${config.label}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
|
||||
fuzzyMatchLabel: true
|
||||
}
|
||||
this.quickOpenService.open({
|
||||
onType: (lookFor, acceptor) => {
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
acceptor(config.values
|
||||
.filter(({ label }) => fuzzyFilter(label))
|
||||
.sort((left, right) => naturalCompare(left.label, right.label))
|
||||
.map(toItem));
|
||||
}
|
||||
}, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BoardsQuickOpenService {
|
||||
export type Item = AvailableBoard & { port: Port } | Board.Detailed | ConfigOption;
|
||||
}
|
108
arduino-ide-extension/src/browser/contributions/about.ts
Normal file
108
arduino-ide-extension/src/browser/contributions/about.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import * as moment from 'moment';
|
||||
import { remote } from 'electron';
|
||||
import { isOSX, isWindows } from '@theia/core/lib/common/os';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { Contribution, Command, MenuModelRegistry, CommandRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ConfigService } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class About extends Contribution {
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(About.Commands.ABOUT_APP, {
|
||||
execute: () => this.showAbout()
|
||||
});
|
||||
}
|
||||
|
||||
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}` : ''}`,
|
||||
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)})` : ''}
|
||||
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
|
||||
|
||||
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}` : ''}`,
|
||||
type: 'info',
|
||||
detail: detail(true),
|
||||
buttons,
|
||||
noLink: true,
|
||||
defaultId: buttons.indexOf(ok),
|
||||
cancelId: buttons.indexOf(ok)
|
||||
});
|
||||
|
||||
if (buttons[response] === copy) {
|
||||
await this.clipboardService.writeText(detail(false));
|
||||
}
|
||||
}
|
||||
|
||||
protected get applicationName(): string {
|
||||
return FrontendApplicationConfigProvider.get().applicationName;
|
||||
}
|
||||
|
||||
protected get buildDate(): string | undefined {
|
||||
return FrontendApplicationConfigProvider.get().buildDate;
|
||||
}
|
||||
|
||||
protected ago(isoTime: string): string {
|
||||
const now = moment(Date.now());
|
||||
const other = moment(isoTime);
|
||||
let result = now.diff(other, 'minute');
|
||||
if (result < 60) {
|
||||
return result === 1 ? `${result} minute ago` : `${result} minute ago`;
|
||||
}
|
||||
result = now.diff(other, 'hour');
|
||||
if (result < 25) {
|
||||
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
|
||||
}
|
||||
result = now.diff(other, 'day');
|
||||
if (result < 8) {
|
||||
return result === 1 ? `${result} day ago` : `${result} days ago`;
|
||||
}
|
||||
result = now.diff(other, 'week');
|
||||
if (result < 5) {
|
||||
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
|
||||
}
|
||||
result = now.diff(other, 'month');
|
||||
if (result < 13) {
|
||||
return result === 1 ? `${result} month ago` : `${result} months ago`;
|
||||
}
|
||||
result = now.diff(other, 'year');
|
||||
return result === 1 ? `${result} year ago` : `${result} years ago`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace About {
|
||||
export namespace Commands {
|
||||
export const ABOUT_APP: Command = {
|
||||
id: 'arduino-about'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class BurnBootloader extends SketchContribution {
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||
execute: () => this.burnBootloader()
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
||||
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
||||
label: 'Burn Bootloader',
|
||||
order: 'z99'
|
||||
});
|
||||
}
|
||||
|
||||
async burnBootloader(): Promise<void> {
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const port = boardsConfig.selectedPort?.address;
|
||||
const [fqbn, { selectedProgrammer: programmer }] = await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
|
||||
]);
|
||||
this.outputChannelManager.getChannel('Arduino: bootloader').clear();
|
||||
await this.coreService.burnBootloader({
|
||||
fqbn,
|
||||
programmer,
|
||||
port
|
||||
});
|
||||
this.messageService.info('Done burning bootloader.', { timeout: 1000 });
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BurnBootloader {
|
||||
export namespace Commands {
|
||||
export const BURN_BOOTLOADER: Command = {
|
||||
id: 'arduino-burn-bootloader'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
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';
|
||||
|
||||
@injectable()
|
||||
export class CloseSketch extends SketchContribution {
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
if (isTemp && await this.wasTouched(uri)) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ["Don't Save", 'Cancel', 'Save'],
|
||||
message: 'Do you want to save changes to this sketch before closing?',
|
||||
detail: "If you don't save, your changes will be lost."
|
||||
});
|
||||
if (response === 1) { // Cancel
|
||||
return;
|
||||
}
|
||||
if (response === 2) { // Save
|
||||
const saved = await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { openAfterMove: false, execOnlyIfTemp: true });
|
||||
if (!saved) { // If it was not saved, do bail the close.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: CloseSketch.Commands.CLOSE_SKETCH.id,
|
||||
label: 'Close',
|
||||
order: '5'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: CloseSketch.Commands.CLOSE_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+W'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
||||
*/
|
||||
protected async wasTouched(uri: string): Promise<boolean> {
|
||||
const editorWidget = await this.editorManager.getByUri(new URI(uri));
|
||||
if (editorWidget) {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace CloseSketch {
|
||||
export namespace Commands {
|
||||
export const CLOSE_SKETCH: Command = {
|
||||
id: 'arduino-close-sketch'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
import { inject, injectable, interfaces } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
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 { 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 { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
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';
|
||||
|
||||
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
|
||||
|
||||
@injectable()
|
||||
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class SketchContribution extends Contribution {
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
}
|
||||
|
||||
export namespace Contribution {
|
||||
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: typeof Contribution): void {
|
||||
bind(serviceIdentifier).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(serviceIdentifier);
|
||||
bind(MenuContribution).toService(serviceIdentifier);
|
||||
bind(KeybindingContribution).toService(serviceIdentifier);
|
||||
bind(TabBarToolbarContribution).toService(serviceIdentifier);
|
||||
bind(FrontendApplicationContribution).toService(serviceIdentifier);
|
||||
}
|
||||
}
|
134
arduino-ide-extension/src/browser/contributions/debug.ts
Normal file
134
arduino-ide-extension/src/browser/contributions/debug.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { URI, Command, CommandRegistry, SketchContribution, TabBarToolbarRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected readonly executableService: ExecutableService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
|
||||
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
protected onDisabledMessageDidChange = this.disabledMessageDidChangeEmitter.event;
|
||||
|
||||
protected get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
}
|
||||
protected set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
}
|
||||
|
||||
protected readonly debugToolbarItem = {
|
||||
id: Debug.Commands.START_DEBUGGING.id,
|
||||
command: Debug.Commands.START_DEBUGGING.id,
|
||||
tooltip: `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`,
|
||||
priority: 3,
|
||||
onDidChange: this.onDisabledMessageDidChange as Event<void>
|
||||
};
|
||||
|
||||
onStart(): void {
|
||||
this.onDisabledMessageDidChange(() => this.debugToolbarItem.tooltip = `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`);
|
||||
const refreshState = async (board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard) => {
|
||||
if (!board) {
|
||||
this.disabledMessage = 'No board selected';
|
||||
return;
|
||||
}
|
||||
const fqbn = board.fqbn;
|
||||
if (!fqbn) {
|
||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
||||
return;
|
||||
}
|
||||
const details = await this.boardService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
||||
return;
|
||||
}
|
||||
const { debuggingSupported } = details;
|
||||
if (!debuggingSupported) {
|
||||
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
|
||||
} else {
|
||||
this.disabledMessage = undefined;
|
||||
}
|
||||
}
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => refreshState(selectedBoard));
|
||||
this.notificationCenter.onPlatformInstalled(() => refreshState());
|
||||
this.notificationCenter.onPlatformUninstalled(() => refreshState());
|
||||
refreshState();
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
|
||||
execute: () => this.startDebug(),
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.disabledMessage
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem(this.debugToolbarItem);
|
||||
}
|
||||
|
||||
protected async startDebug(board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard): Promise<void> {
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const { name, fqbn } = board;
|
||||
if (!fqbn) {
|
||||
return;
|
||||
}
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const [sketch, executables] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.executableService.list()
|
||||
]);
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const [cliPath, sketchPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(executables.cliUri)),
|
||||
this.fileService.fsPath(new URI(sketch.uri))
|
||||
])
|
||||
const config = {
|
||||
cliPath,
|
||||
board: {
|
||||
fqbn,
|
||||
name
|
||||
},
|
||||
sketchPath
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace Debug {
|
||||
export namespace Commands {
|
||||
export const START_DEBUGGING: Command = {
|
||||
id: 'arduino-start-debug',
|
||||
label: 'Start Debugging',
|
||||
category: 'Arduino'
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,277 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
||||
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';
|
||||
|
||||
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
|
||||
// Depends on https://github.com/eclipse-theia/theia/pull/7964
|
||||
@injectable()
|
||||
export class EditContributions extends Contribution {
|
||||
|
||||
@inject(MonacoEditorService)
|
||||
protected readonly codeEditorService: MonacoEditorService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { execute: () => this.run('editor.action.gotoLine') });
|
||||
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, { execute: () => this.run('editor.action.commentLine') });
|
||||
registry.registerCommand(EditContributions.Commands.INDENT_LINES, { execute: () => this.run('editor.action.indentLines') });
|
||||
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, { execute: () => this.run('editor.action.outdentLines') });
|
||||
registry.registerCommand(EditContributions.Commands.FIND, { execute: () => this.run('actions.find') });
|
||||
registry.registerCommand(EditContributions.Commands.FIND_NEXT, { execute: () => this.run('actions.findWithSelection') });
|
||||
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)
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: () => this.preferences.set('editor.fontSize', this.preferences.get('editor.fontSize', EDITOR_FONT_DEFAULTS.fontSize) - 1)
|
||||
});
|
||||
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
|
||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`[code]
|
||||
${value}
|
||||
[/code]`)
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`\`\`\`cpp
|
||||
${value}
|
||||
\`\`\``)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.CUT.id,
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.COPY.id,
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
label: 'Copy for Forum',
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
label: 'Copy for GitHub',
|
||||
order: '3'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.PASTE.id,
|
||||
order: '4'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.SELECT_ALL.id,
|
||||
order: '5'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.GO_TO_LINE.id,
|
||||
label: 'Go to Line...',
|
||||
order: '6'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
label: 'Comment/Uncomment',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INDENT_LINES.id,
|
||||
label: 'Increase Indent',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
||||
label: 'Decrease Indent',
|
||||
order: '2'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: 'Increase Font Size',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: 'Decrease Font Size',
|
||||
order: '1'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND.id,
|
||||
label: 'Find',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_NEXT.id,
|
||||
label: 'Find Next',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
label: 'Find Previous',
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '3'
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
keybinding: 'CtrlCmd+Shift+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
keybinding: 'CtrlCmd+Alt+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||
keybinding: 'CtrlCmd+L',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
keybinding: 'CtrlCmd+/',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+='
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_NEXT.id,
|
||||
keybinding: 'CtrlCmd+G'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
keybinding: 'CtrlCmd+Shift+G'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
keybinding: 'CtrlCmd+E'
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
keybinding: 'CtrlCmd+T'
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
|
||||
return this.codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getActiveCodeEditor();
|
||||
}
|
||||
|
||||
protected async currentValue(): Promise<string | undefined> {
|
||||
const currentEditor = await this.current();
|
||||
if (currentEditor) {
|
||||
const selection = currentEditor.getSelection();
|
||||
if (!selection || selection.isEmpty()) {
|
||||
return currentEditor.getValue();
|
||||
}
|
||||
return currentEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async run(commandId: string): Promise<any> {
|
||||
const editor = await this.current();
|
||||
if (editor) {
|
||||
const action = editor.getAction(commandId);
|
||||
if (action) {
|
||||
return action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace EditContributions {
|
||||
export namespace Commands {
|
||||
export const COPY_FOR_FORUM: Command = {
|
||||
id: 'arduino-copy-for-forum'
|
||||
};
|
||||
export const COPY_FOR_GITHUB: Command = {
|
||||
id: 'arduino-copy-for-github'
|
||||
};
|
||||
export const GO_TO_LINE: Command = {
|
||||
id: 'arduino-go-to-line'
|
||||
};
|
||||
export const TOGGLE_COMMENT: Command = {
|
||||
id: 'arduino-toggle-comment'
|
||||
};
|
||||
export const INDENT_LINES: Command = {
|
||||
id: 'arduino-indent-lines'
|
||||
};
|
||||
export const OUTDENT_LINES: Command = {
|
||||
id: 'arduino-outdent-lines'
|
||||
};
|
||||
export const FIND: Command = {
|
||||
id: 'arduino-find'
|
||||
};
|
||||
export const FIND_NEXT: Command = {
|
||||
id: 'arduino-find-next'
|
||||
};
|
||||
export const FIND_PREVIOUS: Command = {
|
||||
id: 'arduino-find-previous'
|
||||
};
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find'
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size'
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size'
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format' // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
}
|
||||
}
|
151
arduino-ide-extension/src/browser/contributions/examples.ts
Normal file
151
arduino-ide-extension/src/browser/contributions/examples.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { MenuPath, SubMenuOptions, 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 { 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';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
init(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard?.fqbn));
|
||||
}
|
||||
|
||||
protected handleBoardChanged(fqbn: string | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
|
||||
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
|
||||
const groupPath = index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
|
||||
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
|
||||
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
|
||||
parent.addNode(examples);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn('Could not patch menu ordering.');
|
||||
}
|
||||
// Registering the same submenu multiple times has no side-effect.
|
||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', { order: '4' });
|
||||
}
|
||||
|
||||
registerRecursively(
|
||||
exampleContainer: ExampleContainer,
|
||||
menuPath: MenuPath,
|
||||
pushToDispose: DisposableCollection = new DisposableCollection(),
|
||||
options?: SubMenuOptions): 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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BuiltInExamples extends Examples {
|
||||
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
}
|
||||
|
||||
protected async register() {
|
||||
let exampleContainers: ExampleContainer[] | undefined;
|
||||
try {
|
||||
exampleContainers = await this.examplesService.builtIns();
|
||||
} catch (e) {
|
||||
console.error('Could not initialize built-in examples.', e);
|
||||
this.messageService.error('Could not initialize built-in examples.');
|
||||
return;
|
||||
}
|
||||
this.toDispose.dispose();
|
||||
for (const container of exampleContainers) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
|
||||
}
|
||||
this.menuManager.update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
||||
}
|
||||
|
||||
protected handleBoardChanged(fqbn: string | undefined): void {
|
||||
this.register(fqbn);
|
||||
}
|
||||
|
||||
protected async register(fqbn: string | undefined = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn) {
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (!fqbn) {
|
||||
return;
|
||||
}
|
||||
const { user, current, any } = await this.examplesService.installed({ fqbn });
|
||||
for (const container of user) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
|
||||
}
|
||||
for (const container of current) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, this.toDispose);
|
||||
}
|
||||
for (const container of any) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, this.toDispose);
|
||||
}
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,159 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
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 { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { LibraryListWidget } from '../library/library-list-widget';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class IncludeLibrary extends SketchContribution {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
this.updateMenuActions();
|
||||
this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions())
|
||||
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
||||
execute: async arg => {
|
||||
if (LibraryPackage.is(arg)) {
|
||||
this.includeLibrary(arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async updateMenuActions(): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
this.mainMenuManager.update();
|
||||
const libraries: LibraryPackage[] = []
|
||||
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
||||
// Do not show board specific examples, when no board is selected.
|
||||
if (fqbn) {
|
||||
libraries.push(...await this.libraryService.list({ fqbn }));
|
||||
}
|
||||
|
||||
// `Include Library` submenu
|
||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
||||
this.menuRegistry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
|
||||
// `Manage Libraries...` group.
|
||||
this.menuRegistry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
|
||||
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
label: 'Manage Libraries...'
|
||||
});
|
||||
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction({ commandId: `${LibraryListWidget.WIDGET_ID}:toggle` })));
|
||||
|
||||
// `Add .ZIP Library...`
|
||||
// TODO: implement it
|
||||
|
||||
// `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));
|
||||
}
|
||||
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable {
|
||||
const commandId = `arduino-include-library--${library.name}:${library.author}`;
|
||||
const command = { id: commandId };
|
||||
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) };
|
||||
const menuAction = { commandId, label: library.name };
|
||||
this.menuRegistry.registerMenuAction(menuPath, menuAction);
|
||||
return new DisposableCollection(
|
||||
this.commandRegistry.registerCommand(command, handler),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(menuAction)),
|
||||
);
|
||||
}
|
||||
|
||||
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
// If the current editor is one of the additional files from the sketch, we use that.
|
||||
// Otherwise, we pick the editor of the main sketch file.
|
||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
const editor = this.editorManager.currentEditor?.editor;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
if (sketch.additionalFileUris.some(uri => uri === editor.uri.toString())) {
|
||||
codeEditor = editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeEditor) {
|
||||
const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
|
||||
if (widget.editor instanceof MonacoEditor) {
|
||||
codeEditor = widget.editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textModel = codeEditor.getModel();
|
||||
if (!textModel) {
|
||||
return;
|
||||
}
|
||||
const cursorState = codeEditor.getSelections() || [];
|
||||
const eol = textModel.getEOL();
|
||||
const includes = library.includes.slice();
|
||||
includes.push(''); // For the trailing new line.
|
||||
const text = includes.map(include => include ? `#include <${include}>` : eol).join(eol);
|
||||
textModel.pushStackElement(); // Start a fresh operation.
|
||||
textModel.pushEditOperations(cursorState, [{
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text,
|
||||
forceMoveMarkers: true
|
||||
}], () => cursorState);
|
||||
textModel.pushStackElement(); // Make it undoable.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace IncludeLibrary {
|
||||
export namespace Commands {
|
||||
export const INCLUDE_LIBRARY: Command = {
|
||||
id: 'arduino-include-library'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewSketch extends SketchContribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch()
|
||||
});
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: 'New',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewSketch.Commands.NEW_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+N'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
||||
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
||||
tooltip: 'New',
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace NewSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch'
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchExternal extends SketchContribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
||||
execute: () => this.openExternal()
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
label: 'Show Sketch Folder',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
keybinding: 'CtrlCmd+Alt+K'
|
||||
});
|
||||
}
|
||||
|
||||
protected async openExternal(): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (uri) {
|
||||
const exists = this.fileService.exists(new URI(uri));
|
||||
if (exists) {
|
||||
const fsPath = await this.fileService.fsPath(new URI(uri));
|
||||
if (fsPath) {
|
||||
remote.shell.showItemInFolder(fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace OpenSketchExternal {
|
||||
export namespace Commands {
|
||||
export const OPEN_EXTERNAL: Command = {
|
||||
id: 'arduino-open-sketch-external'
|
||||
};
|
||||
}
|
||||
}
|
179
arduino-ide-extension/src/browser/contributions/open-sketch.ts
Normal file
179
arduino-ide-extension/src/browser/contributions/open-sketch.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: arg => Sketch.is(arg) ? this.openSketch(arg) : this.openSketch()
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const sketches = await this.sketchService.getSketches();
|
||||
if (!sketches.length) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: 'Open...'
|
||||
});
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
|
||||
for (const sketch of sketches) {
|
||||
const command = { id: `arduino-open-sketch--${sketch.uri}` };
|
||||
const handler = { execute: () => this.openSketch(sketch) };
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, {
|
||||
commandId: command.id,
|
||||
label: sketch.name
|
||||
});
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
||||
}
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error when collecting built-in examples.', e);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
||||
}
|
||||
}
|
||||
this.contextMenuRenderer.render(options);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: 'Open...',
|
||||
order: '1'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+O'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
tooltip: 'Open',
|
||||
priority: 4
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri));
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Sketch',
|
||||
extensions: ['ino']
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (filePaths.length > 1) {
|
||||
this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`);
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (sketchFileUri.endsWith('.ino')) {
|
||||
const name = new URI(sketchFileUri).path.name;
|
||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: 'Moving',
|
||||
type: 'question',
|
||||
buttons: ['Cancel', 'OK'],
|
||||
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`
|
||||
});
|
||||
if (response === 1) { // OK
|
||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
||||
const exists = await this.fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: `A folder named "${name}" already exists. Can't open sketch.`
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await this.fileService.createFolder(newSketchUri);
|
||||
await this.fileService.move(new URI(sketchFileUri), new URI(newSketchUri.resolve(nameWithExt).toString()));
|
||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace OpenSketch {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch'
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
}
|
46
arduino-ide-extension/src/browser/contributions/quit-app.ts
Normal file
46
arduino-ide-extension/src/browser/contributions/quit-app.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export class QuitApp extends Contribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
||||
execute: () => remote.app.quit()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
||||
if (!isOSX) {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
||||
commandId: QuitApp.Commands.QUIT_APP.id,
|
||||
label: 'Quit',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerKeybinding({
|
||||
command: QuitApp.Commands.QUIT_APP.id,
|
||||
keybinding: 'CtrlCmd+Q'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace QuitApp {
|
||||
export namespace Commands {
|
||||
export const QUIT_APP: Command = {
|
||||
id: 'arduino-quit-app'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
execute: args => this.saveAs(args)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: 'Save As...',
|
||||
order: '7'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Shift+S'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs({ execOnlyIfTemp, openAfterMove, wipeOriginal }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri);
|
||||
const exists = await this.fileService.exists(sketchDirUri.resolve(sketch.name));
|
||||
const defaultUri = exists
|
||||
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
|
||||
: sketchDirUri.resolve(sketch.name);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
|
||||
if (!filePath || canceled) {
|
||||
return false;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
|
||||
if (workspaceUri && openAfterMove) {
|
||||
if (wipeOriginal) {
|
||||
await this.fileService.delete(new URI(sketch.uri));
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
|
||||
}
|
||||
return !!workspaceUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
id: 'arduino-save-as-sketch'
|
||||
};
|
||||
}
|
||||
export interface Options {
|
||||
readonly execOnlyIfTemp?: boolean;
|
||||
readonly openAfterMove?: boolean;
|
||||
/**
|
||||
* Ignored if `openAfterMove` is `false`.
|
||||
*/
|
||||
readonly wipeOriginal?: boolean;
|
||||
}
|
||||
export namespace Options {
|
||||
export const DEFAULT: Options = {
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class SaveSketch extends SketchContribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch()
|
||||
});
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: 'Save',
|
||||
order: '6'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+S'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
||||
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
||||
tooltip: 'Save',
|
||||
priority: 5
|
||||
});
|
||||
}
|
||||
|
||||
async saveSketch(): Promise<void> {
|
||||
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SaveSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch'
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
}
|
38
arduino-ide-extension/src/browser/contributions/settings.ts
Normal file
38
arduino-ide-extension/src/browser/contributions/settings.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { URI, Command, MenuModelRegistry, CommandRegistry, SketchContribution, open } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export class Settings extends SketchContribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Settings.Commands.OPEN_CLI_CONFIG, {
|
||||
execute: () => this.configService.getCliConfigFileUri().then(uri => open(this.openerService, new URI(uri)))
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
|
||||
commandId: CommonCommands.OPEN_PREFERENCES.id,
|
||||
label: 'Preferences...',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
|
||||
commandId: Settings.Commands.OPEN_CLI_CONFIG.id,
|
||||
label: 'Open CLI Configuration',
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace Settings {
|
||||
export namespace Commands {
|
||||
export const OPEN_CLI_CONFIG: Command = {
|
||||
id: 'arduino-open-cli-config',
|
||||
label: 'Open CLI Configuration',
|
||||
category: 'Arduino'
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { URI, SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, open } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export class SketchControl extends SketchContribution {
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, {
|
||||
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id);
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
|
||||
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
const command = { id: `arduino-focus-file--${uri.toString()}` };
|
||||
const handler = { execute: () => open(this.openerService, uri) };
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP, {
|
||||
commandId: command.id,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`
|
||||
});
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
||||
}
|
||||
}
|
||||
this.contextMenuRenderer.render(options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
||||
commandId: WorkspaceCommands.NEW_FILE.id,
|
||||
label: 'New Tab',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: 'Rename',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
|
||||
label: 'Delete',
|
||||
order: '2'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
||||
commandId: CommonCommands.PREVIOUS_TAB.id,
|
||||
label: 'Previous Tab',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
||||
commandId: CommonCommands.NEXT_TAB.id,
|
||||
label: 'Next Tab',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: WorkspaceCommands.NEW_FILE.id,
|
||||
keybinding: 'CtrlCmd+Shift+N'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.PREVIOUS_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Left' // TODO: check why electron does not show the keybindings in the UI.
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.NEXT_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Right'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SketchControl {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch-control--toolbar',
|
||||
iconClass: 'fa fa-caret-down'
|
||||
};
|
||||
}
|
||||
}
|
160
arduino-ide-extension/src/browser/contributions/upload-sketch.ts
Normal file
160
arduino-ide-extension/src/browser/contributions/upload-sketch.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class UploadSketch extends SketchContribution {
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||
execute: () => this.uploadSketch()
|
||||
});
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, {
|
||||
execute: () => this.uploadSketch(true)
|
||||
});
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: 'Upload',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: 'Upload Using Programmer',
|
||||
order: '1'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+U'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
keybinding: 'CtrlCmd+Shift+U'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
||||
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
||||
tooltip: 'Upload',
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
let shouldAutoConnect = false;
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
if (this.monitorConnection.autoConnect) {
|
||||
shouldAutoConnect = true;
|
||||
}
|
||||
this.monitorConnection.autoConnect = false;
|
||||
}
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const [fqbn, { selectedProgrammer }] = await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn)
|
||||
]);
|
||||
|
||||
let options: CoreService.Upload.Options | undefined = undefined;
|
||||
const sketchUri = uri;
|
||||
const optimizeForDebug = this.editorMode.compileForDebug;
|
||||
const { selectedPort } = boardsConfig;
|
||||
const port = selectedPort?.address;
|
||||
|
||||
if (usingProgrammer) {
|
||||
const programmer = selectedProgrammer;
|
||||
options = {
|
||||
sketchUri,
|
||||
fqbn,
|
||||
optimizeForDebug,
|
||||
programmer,
|
||||
port
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
sketchUri,
|
||||
fqbn,
|
||||
optimizeForDebug,
|
||||
port
|
||||
};
|
||||
}
|
||||
this.outputChannelManager.getChannel('Arduino: upload').clear();
|
||||
if (usingProgrammer) {
|
||||
await this.coreService.uploadUsingProgrammer(options);
|
||||
} else {
|
||||
await this.coreService.upload(options);
|
||||
}
|
||||
this.messageService.info('Done uploading.', { timeout: 1000 });
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
const { board, port } = monitorConfig;
|
||||
try {
|
||||
await this.boardsServiceClientImpl.waitUntilAvailable(Object.assign(board, { port }), 10_000);
|
||||
if (shouldAutoConnect) {
|
||||
// Enabling auto-connect will trigger a connect.
|
||||
this.monitorConnection.autoConnect = true;
|
||||
} else {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
} catch (waitError) {
|
||||
this.messageService.error(`Could not reconnect to serial monitor. ${waitError.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace UploadSketch {
|
||||
export namespace Commands {
|
||||
export const UPLOAD_SKETCH: Command = {
|
||||
id: 'arduino-upload-sketch'
|
||||
};
|
||||
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
|
||||
id: 'arduino-upload-sketch-using-programmer'
|
||||
};
|
||||
export const UPLOAD_SKETCH_TOOLBAR: Command = {
|
||||
id: 'arduino-upload-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class VerifySketch extends SketchContribution {
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
||||
execute: () => this.verifySketch()
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
|
||||
label: 'Verify/Compile',
|
||||
order: '2'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: VerifySketch.Commands.VERIFY_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+R'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
||||
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
||||
tooltip: 'Verify',
|
||||
priority: 0
|
||||
});
|
||||
}
|
||||
|
||||
async verifySketch(): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn);
|
||||
this.outputChannelManager.getChannel('Arduino: compile').clear();
|
||||
await this.coreService.compile({
|
||||
sketchUri: uri,
|
||||
fqbn,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
this.messageService.info('Done compiling.', { timeout: 1000 });
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace VerifySketch {
|
||||
export namespace Commands {
|
||||
export const VERIFY_SKETCH: Command = {
|
||||
id: 'arduino-verify-sketch'
|
||||
};
|
||||
export const VERIFY_SKETCH_TOOLBAR: Command = {
|
||||
id: 'arduino-verify-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { AboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
|
||||
import { ConfigService } from '../../common/protocol/config-service';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoAboutDialog extends AboutDialog {
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
const [, version] = await Promise.all([super.init(), this.configService.getVersion()]);
|
||||
if (version) {
|
||||
const { firstChild } = this.contentNode;
|
||||
if (firstChild instanceof HTMLElement && firstChild.classList.contains(ABOUT_CONTENT_CLASS)) {
|
||||
const cliVersion = document.createElement('div');
|
||||
cliVersion.textContent = version;
|
||||
firstChild.appendChild(cliVersion);
|
||||
// TODO: anchor to the commit in the `arduino-cli` repository.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoApplicationShell extends ApplicationShell {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected refreshBottomPanelToggleButton() {
|
||||
if (this.editorMode.proMode) {
|
||||
super.refreshBottomPanelToggleButton();
|
||||
}
|
||||
}
|
||||
|
||||
protected async track(widget: Widget): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
super.track(widget);
|
||||
} else {
|
||||
const tracker = (this as any).tracker as FocusTracker<Widget>;
|
||||
tracker.add(widget);
|
||||
this.disableClose(Saveable.apply(widget));
|
||||
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
|
||||
for (const toTrack of await widget.getTrackableWidgets()) {
|
||||
tracker.add(toTrack);
|
||||
this.disableClose(Saveable.apply(toTrack));
|
||||
}
|
||||
if (widget.onDidChangeTrackableWidgets) {
|
||||
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private disableClose(widget: Widget | undefined): void {
|
||||
if (widget instanceof EditorWidget) {
|
||||
const onCloseRequest = (_: Message) => {
|
||||
// NOOP
|
||||
};
|
||||
(widget as any).onCloseRequest = onCloseRequest.bind(widget);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendApplication extends FrontendApplication {
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(ArduinoFrontendContribution)
|
||||
protected readonly frontendContribution: ArduinoFrontendContribution;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected async initializeLayout(): Promise<void> {
|
||||
return super.initializeLayout().then(() => {
|
||||
// If not in PRO mode, we open the sketch file with all the related files.
|
||||
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
|
||||
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
|
||||
if (!this.editorMode.proMode) {
|
||||
this.workspaceService.roots.then(roots => {
|
||||
for (const root of roots) {
|
||||
this.fileSystem.exists(root.uri).then(exists => {
|
||||
if (exists) {
|
||||
this.frontendContribution.openSketchFiles(root.uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
|
||||
|
||||
export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution {
|
||||
|
||||
protected setConfigTabSizeWidget() {
|
||||
}
|
||||
|
||||
protected setLineEndingWidget() {
|
||||
}
|
||||
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoNavigatorContribution extends FileNavigatorContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
super.registerToolbarItems(toolbarRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -108,7 +108,9 @@
|
||||
"secondaryButton.hoverBackground": "#dae3e3",
|
||||
"arduino.branding.primary": "#00979d",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1"
|
||||
"arduino.foreground": "#edf1f1",
|
||||
"arduino.output.foreground": "#FFFFFF",
|
||||
"arduino.output.background": "#000000"
|
||||
},
|
||||
"type": "light",
|
||||
"name": "Arduino"
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
|
||||
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
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 {
|
||||
|
||||
readonly menuContentChanged = new Emitter<void>();
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
protected app: FrontendApplication;
|
||||
|
||||
@@ -41,7 +41,7 @@ export class EditorMode implements FrontendApplicationContribution {
|
||||
// `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 ArduinoShellLayoutRestorer
|
||||
const layoutRestorer = (this.app as any).layoutRestorer as { storeLayoutAsync(app: FrontendApplication): Promise<void> };
|
||||
await layoutRestorer.storeLayoutAsync(this.app);
|
||||
window.location.reload(true);
|
||||
}
|
||||
@@ -62,6 +62,7 @@ export class EditorMode implements FrontendApplicationContribution {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,34 +0,0 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { ConfigService } from '../../common/protocol/config-service';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoMonacoEditorProvider extends MonacoEditorProvider {
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
protected dataDirUri: string | undefined;
|
||||
|
||||
protected async getModel(uri: URI, toDispose: DisposableCollection): Promise<MonacoEditorModel> {
|
||||
// `createMonacoEditorOptions` is not `async` so we ask the `dataDirUri` here.
|
||||
// https://github.com/eclipse-theia/theia/issues/6234
|
||||
const { dataDirUri } = await this.configService.getConfiguration()
|
||||
this.dataDirUri = dataDirUri;
|
||||
return super.getModel(uri, toDispose);
|
||||
}
|
||||
|
||||
protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions {
|
||||
const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId);
|
||||
options.model = model.textEditorModel;
|
||||
options.readOnly = model.readOnly;
|
||||
if (this.dataDirUri) {
|
||||
options.readOnly = new URI(this.dataDirUri).isEqualOrParent(new URI(model.uri));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
}
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
10
arduino-ide-extension/src/browser/icons/monitor-tab-icon.svg
Normal file
10
arduino-ide-extension/src/browser/icons/monitor-tab-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="24" height="24" viewBox="0, 0, 24, 24">
|
||||
<g>
|
||||
<path d="M16.2 8.1c0-4.5-3.6-8.1-8.1-8.1S0 3.6 0 8.1s3.6 8.1 8.1 8.1c1.9 0 3.7-.7 5.1-1.8l5.6 5.6 1.4-1.4-5.7-5.6c1.1-1.4 1.7-3.1 1.7-4.9zm-14.4 0c0-3.5 2.8-6.3 6.3-6.3s6.3 2.8 6.3 6.3-2.8 6.3-6.3 6.3c-3.5.1-6.3-2.8-6.3-6.3z" />
|
||||
<rect x="7.1" y="7.1" width="2" height="2" />
|
||||
<rect x="17.2" y="7.1" width="2" height="2" />
|
||||
<rect x="20.3" y="7.1" width="2" height="2" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 711 B |
@@ -1,53 +0,0 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { Board, BoardPackage } from '../../common/protocol/boards-service';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
|
||||
|
||||
readonly id = 'ino';
|
||||
readonly name = 'Arduino';
|
||||
|
||||
protected get documentSelector(): string[] {
|
||||
return ['ino'];
|
||||
}
|
||||
|
||||
protected get globPatterns() {
|
||||
return ['**/*.ino'];
|
||||
}
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
protected boardConfig?: BoardsConfig.Config;
|
||||
|
||||
@postConstruct()
|
||||
protected init() {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
|
||||
const restartIfAffected = (pkg: BoardPackage) => {
|
||||
if (!this.boardConfig) {
|
||||
this.restart();
|
||||
return;
|
||||
}
|
||||
const { selectedBoard } = this.boardConfig;
|
||||
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
}
|
||||
|
||||
selectBoard(config: BoardsConfig.Config): void {
|
||||
this.boardConfig = config;
|
||||
// Force a restart to send the new board config to the language server
|
||||
this.restart();
|
||||
}
|
||||
|
||||
protected getStartParameters(): BoardsConfig.Config | undefined {
|
||||
return this.boardConfig;
|
||||
}
|
||||
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLanguageGrammarContribution implements LanguageGrammarDefinitionContribution {
|
||||
|
||||
static INO_LANGUAGE_ID = 'ino';
|
||||
|
||||
registerTextmateLanguage(registry: TextmateRegistry) {
|
||||
monaco.languages.register({
|
||||
id: ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID,
|
||||
extensions: ['.ino'],
|
||||
aliases: ['INO', 'Ino', 'ino'],
|
||||
});
|
||||
|
||||
monaco.languages.setLanguageConfiguration(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, this.configuration);
|
||||
|
||||
const inoGrammar = require('../../../data/ino.tmLanguage.json');
|
||||
registry.registerTextmateGrammarScope('source.ino', {
|
||||
async getGrammarDefinition() {
|
||||
return {
|
||||
format: 'json',
|
||||
content: inoGrammar
|
||||
};
|
||||
}
|
||||
});
|
||||
registry.mapLanguageIdToTextmateGrammar(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, 'source.ino');
|
||||
}
|
||||
|
||||
private readonly configuration: monaco.languages.LanguageConfiguration = {
|
||||
comments: {
|
||||
lineComment: '//',
|
||||
blockComment: ['/*', '*/'],
|
||||
},
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')']
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
|
||||
{ open: '"', close: '"', notIn: ['string'] },
|
||||
{ open: '/*', close: ' */', notIn: ['string'] }
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: '\'', close: '\'' },
|
||||
],
|
||||
folding: {
|
||||
markers: {
|
||||
start: new RegExp('^\\s*#pragma\\s+region\\b'),
|
||||
end: new RegExp('^\\s*#pragma\\s+endregion\\b')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Library, LibraryService } from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../components/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
import { injectable, postConstruct, inject } from 'inversify';
|
||||
import { LibraryPackage, LibraryService } from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<Library> {
|
||||
export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
|
||||
static WIDGET_ID = 'library-list-widget';
|
||||
static WIDGET_LABEL = 'Library Manager';
|
||||
|
||||
constructor(
|
||||
@inject(LibraryService) protected service: LibraryService,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<LibraryPackage>) {
|
||||
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
@@ -19,9 +19,18 @@ export class LibraryListWidget extends ListWidget<Library> {
|
||||
iconClass: 'library-tab-icon',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: Library) => item.name,
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemRenderer
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.refresh(undefined)),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import { ArduinoMenus } from '../arduino-frontend-contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution extends AbstractViewContribution<LibraryListWidget> implements FrontendApplicationContribution {
|
||||
@@ -17,7 +17,7 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut
|
||||
rank: 700
|
||||
},
|
||||
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'ctrlcmd+shift+l'
|
||||
toggleKeybinding: 'CtrlCmd+Shift+I'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS, {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: this.toggleCommand.id,
|
||||
label: 'Manage Libraries...'
|
||||
label: 'Manage Libraries...',
|
||||
order: '3'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
66
arduino-ide-extension/src/browser/menu/arduino-menus.ts
Normal file
66
arduino-ide-extension/src/browser/menu/arduino-menus.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
|
||||
// Main menu
|
||||
// -- File
|
||||
export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch'];
|
||||
export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print'];
|
||||
// XXX: on macOS, the settings group is not under `File`
|
||||
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
|
||||
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
|
||||
|
||||
// -- File / Examples
|
||||
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '0_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'];
|
||||
export const EXAMPLES__USER_LIBS_GROUP = [...FILE__EXAMPLES_SUBMENU, '3_user_libs'];
|
||||
|
||||
// -- Edit
|
||||
// `Copy`, `Copy to Forum`, `Paste`, etc.
|
||||
// Note: `1_undo` is the first group from Theia, we start with `2`
|
||||
export const EDIT__TEXT_CONTROL_GROUP = [...CommonMenus.EDIT, '2_text_control'];
|
||||
// `Comment/Uncomment`, etc.
|
||||
export const EDIT__CODE_CONTROL_GROUP = [...CommonMenus.EDIT, '3_code_control'];
|
||||
export const EDIT__FONT_CONTROL_GROUP = [...CommonMenus.EDIT, '4_font_control'];
|
||||
export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find'];
|
||||
|
||||
// -- Sketch
|
||||
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
|
||||
export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main'];
|
||||
export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils'];
|
||||
|
||||
// -- Tools
|
||||
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
|
||||
// `Auto Format`, `Library Manager...`, `Boards Manager...`
|
||||
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
|
||||
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
||||
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '1_board_settings'];
|
||||
|
||||
// -- Help
|
||||
// `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'];
|
||||
|
||||
// ------------
|
||||
|
||||
// Context menus
|
||||
// -- Open
|
||||
export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context'];
|
||||
export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [...OPEN_SKETCH__CONTEXT, '0_open'];
|
||||
export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [...OPEN_SKETCH__CONTEXT, '1_recent'];
|
||||
export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [...OPEN_SKETCH__CONTEXT, '2_examples'];
|
||||
|
||||
// -- Sketch control
|
||||
export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context'];
|
||||
// `New Tab`, `Rename`, `Delete`
|
||||
export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [...SKETCH_CONTROL__CONTEXT, '0_main'];
|
||||
// `Previous Tab`, `Next Tab`
|
||||
export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [...SKETCH_CONTROL__CONTEXT, '1_navigation'];
|
||||
// Sketch files opened in editors
|
||||
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [...SKETCH_CONTROL__CONTEXT, '2_resources'];
|
||||
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { ArduinoMenuContribution } from './arduino-menu-contribution';
|
||||
import { ContainerModule, interfaces } from 'inversify';
|
||||
|
||||
import '../../../src/browser/style/browser-menu.css'
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
|
||||
unbind(BrowserMenuBarContribution);
|
||||
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
|
||||
});
|
@@ -1,13 +1,15 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class MonitorConnection {
|
||||
@@ -24,8 +26,11 @@ export class MonitorConnection {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
@@ -109,13 +114,13 @@ export class MonitorConnection {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
|
||||
this.boardsServiceClient.onBoardsChanged(event => {
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
|
||||
this.notificationCenter.onAttachedBoardsChanged(event => {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
|
||||
if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect()
|
||||
@@ -127,7 +132,7 @@ export class MonitorConnection {
|
||||
// Handles the `baudRate` changes by reconnecting if required.
|
||||
this.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'baudRate' && this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
}
|
||||
});
|
||||
@@ -153,7 +158,7 @@ export class MonitorConnection {
|
||||
// We have to make sure the previous boards config has been restored.
|
||||
// Otherwise, we might start the auto-connection without configured boards.
|
||||
this.applicationState.reachedState('started_contributions').then(() => {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
});
|
||||
} else if (oldValue && !value) {
|
||||
@@ -182,15 +187,19 @@ export class MonitorConnection {
|
||||
}
|
||||
|
||||
async disconnect(): Promise<Status> {
|
||||
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
|
||||
if (!this.connected) {
|
||||
return Status.OK;
|
||||
}
|
||||
const stateCopy = deepClone(this.state);
|
||||
if (!stateCopy) {
|
||||
return Status.OK;
|
||||
}
|
||||
console.log('>>> Disposing existing monitor connection...');
|
||||
const status = await this.monitorService.disconnect();
|
||||
if (Status.isOK(status)) {
|
||||
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
|
||||
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(stateCopy)}`);
|
||||
} else {
|
||||
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
|
||||
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(stateCopy)}`);
|
||||
}
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
@@ -222,10 +231,10 @@ export class MonitorConnection {
|
||||
|
||||
protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise<void> {
|
||||
if (this.autoConnect) {
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) {
|
||||
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
|
||||
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
|
||||
this.boardsService.getAvailablePorts().then(({ ports }) => {
|
||||
this.boardsService.getAvailablePorts().then(ports => {
|
||||
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
|
||||
new Promise<void>(resolve => {
|
||||
// First, disconnect if connected.
|
||||
|
@@ -2,7 +2,7 @@ import { injectable, inject } from 'inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class MonitorModel implements FrontendApplicationContribution {
|
||||
@@ -12,8 +12,8 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
@inject(LocalStorageService)
|
||||
protected readonly localStorageService: LocalStorageService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected readonly onChangeEmitter: Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>;
|
||||
protected _autoscroll: boolean;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, inject } from "inversify";
|
||||
import { AbstractViewContribution } from "@theia/core/lib/browser";
|
||||
import { MonitorWidget } from "./monitor-widget";
|
||||
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
|
||||
import { ArduinoMenus } from "../arduino-frontend-contribution";
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser';
|
||||
import { MonitorWidget } from './monitor-widget';
|
||||
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
export namespace SerialMonitor {
|
||||
export namespace Commands {
|
||||
@@ -42,15 +42,16 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
area: 'bottom'
|
||||
},
|
||||
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
|
||||
toggleKeybinding: 'ctrlcmd+shift+m'
|
||||
toggleKeybinding: 'CtrlCmd+Shift+M'
|
||||
})
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS, {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: this.toggleCommand.id,
|
||||
label: 'Serial Monitor'
|
||||
label: 'Serial Monitor',
|
||||
order: '5'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -86,25 +87,23 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
}
|
||||
});
|
||||
if (this.toggleCommand) {
|
||||
commands.registerCommand(this.toggleCommand, {
|
||||
execute: () => this.openView({
|
||||
toggle: true,
|
||||
activate: true
|
||||
})
|
||||
});
|
||||
const toolbarCmd = {
|
||||
id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR
|
||||
}
|
||||
commands.registerCommand(toolbarCmd, {
|
||||
commands.registerCommand(this.toggleCommand, { execute: () => this.toggle() });
|
||||
commands.registerCommand({ id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR }, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
|
||||
execute: () => this.openView({
|
||||
toggle: true,
|
||||
activate: true
|
||||
})
|
||||
execute: () => this.toggle()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async toggle(): Promise<void> {
|
||||
const widget = this.tryGetWidget();
|
||||
if (widget) {
|
||||
widget.dispose();
|
||||
} else {
|
||||
await this.openView({ activate: true, reveal: true });
|
||||
}
|
||||
}
|
||||
|
||||
protected renderAutoScrollButton(): React.ReactNode {
|
||||
return <React.Fragment key='autoscroll-toolbar-item'>
|
||||
<div
|
||||
|
@@ -5,11 +5,11 @@ import { OptionsType } from 'react-select/src/types';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable'
|
||||
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'
|
||||
import { ReactWidget, Message, Widget, MessageLoop } from '@theia/core/lib/browser/widgets';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { ArduinoSelect } from '../components/arduino-select';
|
||||
import { ArduinoSelect } from '../widgets/arduino-select';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
@@ -45,10 +45,16 @@ export class MonitorWidget extends ReactWidget {
|
||||
super();
|
||||
this.id = MonitorWidget.ID;
|
||||
this.title.label = 'Serial Monitor';
|
||||
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
|
||||
this.title.iconClass = 'monitor-tab-icon';
|
||||
this.title.closable = true;
|
||||
this.scrollOptions = undefined;
|
||||
this.toDispose.push(this.clearOutputEmitter);
|
||||
this.toDispose.push(Disposable.create(() => {
|
||||
this.monitorConnection.autoConnect = false;
|
||||
if (this.monitorConnection.connected) {
|
||||
this.monitorConnection.disconnect();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
@@ -73,10 +79,6 @@ export class MonitorWidget extends ReactWidget {
|
||||
|
||||
onCloseRequest(msg: Message): void {
|
||||
this.closing = true;
|
||||
this.monitorConnection.autoConnect = false;
|
||||
if (this.monitorConnection.connected) {
|
||||
this.monitorConnection.disconnect();
|
||||
}
|
||||
super.onCloseRequest(msg);
|
||||
}
|
||||
|
||||
@@ -100,6 +102,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
}
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined) => {
|
||||
if (this.closing || !this.isAttached) {
|
||||
return;
|
||||
}
|
||||
this.focusNode = element;
|
||||
requestAnimationFrame(() => MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest));
|
||||
}
|
||||
@@ -178,7 +183,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
this.monitorModel.lineEnding = option.value;
|
||||
}
|
||||
|
||||
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||
protected readonly onChangeBaudRate = (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||
this.monitorModel.baudRate = option.value;
|
||||
}
|
||||
|
||||
@@ -288,17 +293,20 @@ export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Pro
|
||||
|
||||
componentDidMount(): void {
|
||||
this.scrollToBottom();
|
||||
let chunk = '';
|
||||
this.toDisposeBeforeUnmount.pushAll([
|
||||
this.props.monitorConnection.onRead(({ data }) => {
|
||||
chunk += data;
|
||||
const eolIndex = chunk.indexOf('\n');
|
||||
if (eolIndex !== -1) {
|
||||
const line = chunk.substring(0, eolIndex + 1);
|
||||
chunk = chunk.slice(eolIndex + 1);
|
||||
const content = `${this.state.content}${this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`;
|
||||
this.setState({ content });
|
||||
const rawLines = data.split('\n');
|
||||
const lines: string[] = []
|
||||
const timestamp = () => this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : '';
|
||||
for (let i = 0; i < rawLines.length; i++) {
|
||||
if (i === 0 && this.state.content.length !== 0) {
|
||||
lines.push(rawLines[i]);
|
||||
} else {
|
||||
lines.push(timestamp() + rawLines[i]);
|
||||
}
|
||||
}
|
||||
const content = this.state.content + lines.join('\n');
|
||||
this.setState({ content });
|
||||
}),
|
||||
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
|
||||
this.props.monitorModel.onChange(({ property }) => {
|
||||
|
92
arduino-ide-extension/src/browser/notification-center.ts
Normal file
92
arduino-ide-extension/src/browser/notification-center.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
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';
|
||||
|
||||
@injectable()
|
||||
export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution {
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
|
||||
|
||||
protected readonly indexUpdatedEmitter = new Emitter<void>();
|
||||
protected readonly daemonStartedEmitter = new Emitter<void>();
|
||||
protected readonly daemonStoppedEmitter = new Emitter<void>();
|
||||
protected readonly configChangedEmitter = new Emitter<{ config: Config | undefined }>();
|
||||
protected readonly platformInstalledEmitter = new Emitter<{ item: BoardsPackage }>();
|
||||
protected readonly platformUninstalledEmitter = new Emitter<{ item: BoardsPackage }>();
|
||||
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.indexUpdatedEmitter,
|
||||
this.daemonStartedEmitter,
|
||||
this.daemonStoppedEmitter,
|
||||
this.configChangedEmitter,
|
||||
this.platformInstalledEmitter,
|
||||
this.platformUninstalledEmitter,
|
||||
this.libraryInstalledEmitter,
|
||||
this.libraryUninstalledEmitter,
|
||||
this.attachedBoardsChangedEmitter
|
||||
);
|
||||
|
||||
readonly onIndexUpdated = this.indexUpdatedEmitter.event;
|
||||
readonly onDaemonStarted = this.daemonStartedEmitter.event;
|
||||
readonly onDaemonStopped = this.daemonStoppedEmitter.event;
|
||||
readonly onConfigChanged = this.configChangedEmitter.event;
|
||||
readonly onPlatformInstalled = this.platformInstalledEmitter.event;
|
||||
readonly onPlatformUninstalled = this.platformUninstalledEmitter.event;
|
||||
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
|
||||
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
|
||||
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.server.setClient(this);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
notifyIndexUpdated(): void {
|
||||
this.indexUpdatedEmitter.fire();
|
||||
}
|
||||
|
||||
notifyDaemonStarted(): void {
|
||||
this.daemonStartedEmitter.fire();
|
||||
}
|
||||
|
||||
notifyDaemonStopped(): void {
|
||||
this.daemonStoppedEmitter.fire();
|
||||
}
|
||||
|
||||
notifyConfigChanged(event: { config: Config | undefined }): void {
|
||||
this.configChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
this.platformInstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
|
||||
this.platformUninstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
|
||||
this.libraryInstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
|
||||
this.libraryUninstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.attachedBoardsChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
}
|
28
arduino-ide-extension/src/browser/output-service-impl.ts
Normal file
28
arduino-ide-extension/src/browser/output-service-impl.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { OutputService, OutputMessage } from '../common/protocol/output-service';
|
||||
|
||||
@injectable()
|
||||
export class OutputServiceImpl implements OutputService {
|
||||
|
||||
@inject(OutputContribution)
|
||||
protected outputContribution: OutputContribution;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
@@ -97,7 +97,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .detail {
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .details {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
width: 155px; /* used heuristics for the calculation */
|
||||
@@ -169,6 +169,13 @@ button.theia-button.main {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .guessed {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: var(--theia-warningBackground);
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
@@ -1,13 +1,6 @@
|
||||
/* Do not show the `close` icon for editor, but show the dirty state if not in pro-mode. */
|
||||
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:hover > .p-TabBar-tabCloseIcon,
|
||||
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon {
|
||||
background-image: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty:hover > .p-TabBar-tabCloseIcon,
|
||||
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
|
||||
background-size: 10px;
|
||||
/* Show the dirty indicator on unclosable widgets. On hover, it should still show the dot instead of the X. */
|
||||
/* https://github.com/arduino/arduino-pro-ide/issues/380 */
|
||||
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
|
||||
background-size: 13px;
|
||||
background-image: var(--theia-icon-circle);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
@import './list-widget.css';
|
||||
@import './board-select-dialog.css';
|
||||
@import './boards-config-dialog.css';
|
||||
@import './main.css';
|
||||
@import './editor.css';
|
||||
@import './monitor.css';
|
||||
@import './arduino-select.css';
|
||||
@import './status-bar.css';
|
||||
@import './terminal.css';
|
||||
@import './editor.css';
|
||||
|
||||
.theia-input.warning:focus {
|
||||
outline-width: 1px;
|
||||
|
@@ -46,7 +46,7 @@
|
||||
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
|
||||
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
|
||||
https://github.com/arduino/arduino-pro-ide/issues/82 */
|
||||
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
|
||||
.arduino-list-widget .filterable-list-container .items-container .ps__rail-y {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@@ -2,15 +2,6 @@
|
||||
background: var(--theia-editorGroupHeader-tabsBackground);
|
||||
}
|
||||
|
||||
.theia-output {
|
||||
background: var(--theia-terminal-background);
|
||||
color: var(--theia-terminal-foreground);
|
||||
}
|
||||
|
||||
#outputView {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item {
|
||||
margin-left: 3px;
|
||||
}
|
||||
@@ -21,14 +12,15 @@
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: var(--theia-button-background);
|
||||
background: var(--theia-arduino-toolbar-background);
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item > div:hover {
|
||||
background: var(--theia-button-hoverBackground);
|
||||
background: (--theia-arduino-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.arduino-verify, .arduino-upload {
|
||||
.arduino-verify-sketch--toolbar,
|
||||
.arduino-upload-sketch--toolbar {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@@ -42,22 +34,27 @@
|
||||
mask-size: 800%;
|
||||
}
|
||||
|
||||
.arduino-save-file-icon {
|
||||
.arduino-save-sketch--toolbar-icon {
|
||||
-webkit-mask-position: 59px -4px;
|
||||
mask-position: 59px -4px;
|
||||
}
|
||||
|
||||
.arduino-verify-icon {
|
||||
.arduino-verify-sketch--toolbar-icon {
|
||||
-webkit-mask-position: 188px -4px;
|
||||
mask-position: 188px -4px;
|
||||
}
|
||||
|
||||
.arduino-upload-icon {
|
||||
.arduino-upload-sketch--toolbar-icon {
|
||||
-webkit-mask-position: 156px -4px;
|
||||
mask-position: 156px -4px;
|
||||
}
|
||||
|
||||
.arduino-show-open-context-menu-icon {
|
||||
.arduino-new-sketch--toolbar-icon {
|
||||
-webkit-mask-position: 124px -4px;
|
||||
mask-position: 124px -4px;
|
||||
}
|
||||
|
||||
.arduino-open-sketch--toolbar-icon {
|
||||
-webkit-mask-position: 92px -4px;
|
||||
mask-position: 92px -4px;
|
||||
}
|
||||
@@ -67,6 +64,23 @@
|
||||
mask-position: 28px -4px;
|
||||
}
|
||||
|
||||
.arduino-start-debug-icon {
|
||||
-webkit-mask: url('../icons/debug-dark.svg') 50%;
|
||||
mask: url('../icons/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;
|
||||
}
|
||||
|
||||
#arduino-toolbar-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -117,7 +131,17 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theia-titleBar-activeBackground);;
|
||||
color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
.arduino-open-boards-control-icon {
|
||||
mask: none;
|
||||
-webkit-mask: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
.monaco-editor .margin {
|
||||
@@ -138,3 +162,33 @@
|
||||
.p-Widget.dialogOverlay .dialogBlock {
|
||||
background-color: var(--theia-arduino-foreground);
|
||||
}
|
||||
|
||||
#arduino-open-sketch-control--toolbar--container {
|
||||
background-color: var(--theia-arduino-toolbar-background);
|
||||
padding: 8px 8px 8px 8px; /* based on pure heuristics */
|
||||
}
|
||||
|
||||
#arduino-open-sketch-control--toolbar {
|
||||
height: unset;
|
||||
width: unset;
|
||||
line-height: unset;
|
||||
color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
/* Output */
|
||||
.theia-output .editor-container {
|
||||
background-color: var(--theia-arduino-output-background);
|
||||
}
|
||||
|
||||
.theia-output .monaco-editor .lines-content.monaco-editor-background {
|
||||
background-color: var(--theia-arduino-output-background);
|
||||
}
|
||||
|
||||
.theia-output .monaco-editor .lines-content.monaco-editor-background .view-lines .view-line .mtk1:not(.theia-output-error):not(.theia-output-warning) {
|
||||
color: var(--theia-arduino-output-foreground);
|
||||
}
|
||||
|
||||
.theia-output .monaco-editor .margin {
|
||||
border-right: none;
|
||||
background-color: var(--theia-arduino-output-background);
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
.p-TabBar.theia-app-centers .p-TabBar-tabIcon.arduino-serial-monitor-tab-icon {
|
||||
background: url(../icons/buttons.svg);
|
||||
background-size: 800%;
|
||||
background-position-y: 41px;
|
||||
background-position-x: 19px;
|
||||
.monitor-tab-icon {
|
||||
-webkit-mask: url('../icons/monitor-tab-icon.svg');
|
||||
mask: url('../icons/monitor-tab-icon.svg');
|
||||
}
|
||||
|
||||
.serial-monitor {
|
||||
|
7
arduino-ide-extension/src/browser/style/status-bar.css
Normal file
7
arduino-ide-extension/src/browser/style/status-bar.css
Normal file
@@ -0,0 +1,7 @@
|
||||
#theia-statusBar .area .element.arduino-selected-port {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
#theia-statusBar .area .element.arduino-selected-board > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
3
arduino-ide-extension/src/browser/style/terminal.css
Normal file
3
arduino-ide-extension/src/browser/style/terminal.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.terminal-container .xterm .xterm-helper-textarea {
|
||||
opacity: 0 !important; /* fix secondary cursor-like issue. See https://github.com/eclipse-theia/theia/issues/8158 */
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
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;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(ConnectionStatusService)
|
||||
protected readonly connectionStatusService: ConnectionStatusService;
|
||||
|
||||
protected track(widget: Widget): void {
|
||||
super.track(widget);
|
||||
if (widget instanceof OutputWidget) {
|
||||
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
|
||||
}
|
||||
if (!this.editorMode.proMode && widget instanceof EditorWidget) {
|
||||
// Make the editor un-closeable asynchronously.
|
||||
this.sketchesServiceClient.currentSketch().then(sketch => {
|
||||
if (sketch) {
|
||||
if (Sketch.isInSketch(widget.editor.uri, sketch)) {
|
||||
widget.title.closable = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async addWidget(widget: Widget, options: Readonly<TheiaApplicationShell.WidgetOptions> = {}): Promise<void> {
|
||||
// By default, Theia open a widget **next** to the currently active in the target area.
|
||||
// Instead of this logic, we want to open the new widget after the last of the target area.
|
||||
if (!widget.id) {
|
||||
console.error('Widgets added to the application shell must have a unique id property.');
|
||||
return;
|
||||
}
|
||||
let ref: Widget | undefined = options.ref;
|
||||
let area: TheiaApplicationShell.Area = options.area || 'main';
|
||||
if (!ref && (area === 'main' || area === 'bottom')) {
|
||||
const tabBar = this.getTabBarFor(area);
|
||||
if (tabBar) {
|
||||
const last = tabBar.titles[tabBar.titles.length - 1];
|
||||
if (last) {
|
||||
ref = last.owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.addWidget(widget, { ...options, ref });
|
||||
}
|
||||
|
||||
async saveAll(): Promise<void> {
|
||||
if (this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE) {
|
||||
this.messageService.error('Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.');
|
||||
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803
|
||||
}
|
||||
await super.saveAll();
|
||||
const options = { execOnlyIfTemp: true, openAfterMove: true };
|
||||
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, MenuBarWidget } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||
|
||||
@injectable()
|
||||
export class BrowserMainMenuFactory extends TheiaBrowserMainMenuFactory implements MainMenuManager {
|
||||
|
||||
protected menuBar: MenuBarWidget | undefined;
|
||||
|
||||
createMenuBar(): MenuBarWidget {
|
||||
this.menuBar = super.createMenuBar();
|
||||
return this.menuBar;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.menuBar) {
|
||||
this.menuBar.clearMenus();
|
||||
this.fillMenuBar(this.menuBar);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import '../../../../src/browser/style/browser-menu.css';
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { BrowserMenuBarContribution, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||
import { ArduinoMenuContribution } from './browser-menu-plugin';
|
||||
import { BrowserMainMenuFactory } from './browser-main-menu-factory';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
|
||||
bind(MainMenuManager).toService(BrowserMainMenuFactory);
|
||||
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
|
||||
rebind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
|
||||
});
|
@@ -0,0 +1,29 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { CommonFrontendContribution as TheiaCommonFrontendContribution, CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
super.registerMenus(registry);
|
||||
for (const command of [
|
||||
CommonCommands.SAVE,
|
||||
CommonCommands.SAVE_ALL,
|
||||
CommonCommands.CUT,
|
||||
CommonCommands.COPY,
|
||||
CommonCommands.PASTE,
|
||||
CommonCommands.COPY_PATH,
|
||||
CommonCommands.FIND,
|
||||
CommonCommands.REPLACE,
|
||||
CommonCommands.AUTO_SAVE,
|
||||
CommonCommands.OPEN_PREFERENCES,
|
||||
CommonCommands.SELECT_ICON_THEME,
|
||||
CommonCommands.SELECT_COLOR_THEME,
|
||||
CommonCommands.ABOUT_COMMAND
|
||||
]) {
|
||||
registry.unregisterMenuAction(command);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
|
||||
ConnectionStatus
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import { ArduinoDaemon } from '../../../common/protocol';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected isRunning = false;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
this.schedulePing();
|
||||
try {
|
||||
this.isRunning = await this.daemon.isRunning();
|
||||
} catch { }
|
||||
this.notificationCenter.onDaemonStarted(() => this.isRunning = true);
|
||||
this.notificationCenter.onDaemonStopped(() => this.isRunning = false);
|
||||
this.wsConnectionProvider.onIncomingMessageActivity(() => {
|
||||
this.updateStatus(this.isRunning);
|
||||
this.schedulePing();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected isRunning = false;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
try {
|
||||
this.isRunning = await this.daemon.isRunning();
|
||||
} catch { }
|
||||
this.notificationCenter.onDaemonStarted(() => this.isRunning = true);
|
||||
this.notificationCenter.onDaemonStopped(() => this.isRunning = false);
|
||||
}
|
||||
|
||||
protected onStateChange(state: ConnectionStatus): void {
|
||||
if (!this.isRunning && state === ConnectionStatus.ONLINE) {
|
||||
return;
|
||||
}
|
||||
super.onStateChange(state);
|
||||
}
|
||||
|
||||
protected handleOffline(): void {
|
||||
this.statusBar.setElement('connection-status', {
|
||||
alignment: StatusBarAlignment.LEFT,
|
||||
text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
|
||||
tooltip: this.isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.',
|
||||
priority: 5000
|
||||
});
|
||||
this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement('connection-status')));
|
||||
document.body.classList.add('theia-mod-offline');
|
||||
this.toDisposeOnOnline.push(Disposable.create(() => document.body.classList.remove('theia-mod-offline')));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
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 { ArduinoCommands } from '../../arduino-commands';
|
||||
|
||||
@injectable()
|
||||
export class FrontendApplication extends TheiaFrontendApplication {
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
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) {
|
||||
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
28
arduino-ide-extension/src/browser/theia/core/keybindings.ts
Normal file
28
arduino-ide-extension/src/browser/theia/core/keybindings.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { Keybinding } from '@theia/core/lib/common/keybinding';
|
||||
import { KeybindingRegistry as TheiaKeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding';
|
||||
|
||||
@injectable()
|
||||
export class KeybindingRegistry extends TheiaKeybindingRegistry {
|
||||
|
||||
// https://github.com/eclipse-theia/theia/issues/8209
|
||||
unregisterKeybinding(key: string): void;
|
||||
unregisterKeybinding(keybinding: Keybinding): void;
|
||||
unregisterKeybinding(command: Command): void;
|
||||
unregisterKeybinding(arg: string | Keybinding | Command): void {
|
||||
const keymap = this.keymaps[KeybindingScope.DEFAULT];
|
||||
const filter = Command.is(arg)
|
||||
? ({ command }: Keybinding) => command === arg.id
|
||||
: ({ keybinding }: Keybinding) => Keybinding.is(arg)
|
||||
? keybinding === arg.keybinding
|
||||
: keybinding === arg;
|
||||
for (const binding of keymap.filter(filter)) {
|
||||
const idx = keymap.indexOf(binding);
|
||||
if (idx !== -1) {
|
||||
keymap.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user